@webview-native-ui/engine 0.0.4 → 0.0.6
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/projection/scope.js +1 -1
- package/dist/projection/styles.d.ts +2 -2
- package/dist/projection/styles.js +20 -9
- package/dist/projection/web.js +259 -185
- package/dist/types.d.ts +4 -0
- 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 +30 -16
- package/ios/Sources/WebViewNativeUIEngine/CoveredSnapshots.swift +2 -2
- package/ios/Sources/WebViewNativeUIEngine/NativeUIProjection.swift +35 -22
- package/ios/Sources/WebViewNativeUIEngine/Resources/native-ui-engine.js +20 -12
- package/ios/Sources/WebViewNativeUIEngine/ScrollBindings.swift +1 -0
- package/ios/Sources/WebViewNativeUIEngine/ScrollTracker.swift +1 -1
- package/package.json +2 -2
package/dist/types.d.ts
CHANGED
package/docs/capacitor-plugin.md
CHANGED
|
@@ -277,3 +277,7 @@ If your control continues animating after its update notification, set `captureD
|
|
|
277
277
|
on the native registration (Swift: `captureDelayMs: 600`; Kotlin: `captureDelayMs = 600`).
|
|
278
278
|
This changes background image preparation, not overlay presentation timing.
|
|
279
279
|
See [capture timing](projection.md#capture-timing) for the cache behavior and measured guidance.
|
|
280
|
+
|
|
281
|
+
### Projection DOM
|
|
282
|
+
|
|
283
|
+
The engine keeps a reusable image inside the fallback container, using its open ShadowRoot when available. No placement option is needed. If the renderer inspects child elements, exclude nodes marked `data-native-ui-projection-node`. See [DOM requirements](projection.md#dom-and-native-surface-requirements).
|
package/docs/cordova-plugin.md
CHANGED
|
@@ -196,3 +196,7 @@ If your control continues animating after its update notification, set `captureD
|
|
|
196
196
|
on the native registration (Swift: `captureDelayMs: 600`; Kotlin: `captureDelayMs = 600`).
|
|
197
197
|
This changes background image preparation, not overlay presentation timing.
|
|
198
198
|
See [capture timing](projection.md#capture-timing) for the cache behavior and measured guidance.
|
|
199
|
+
|
|
200
|
+
### Projection DOM
|
|
201
|
+
|
|
202
|
+
The engine keeps a reusable image inside the fallback container, using its open ShadowRoot when available. No placement option is needed. If the renderer inspects child elements, exclude nodes marked `data-native-ui-projection-node`. See [DOM requirements](projection.md#dom-and-native-surface-requirements).
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Projection container support
|
|
2
|
+
|
|
3
|
+
Choose the existing DOM element that occupies the native surface's intended location and Web layer. These tables apply to `projectionSelector` and the element passed to `createWebProjectionScope`, for both Capacitor and Cordova.
|
|
4
|
+
|
|
5
|
+
**⭕️** means the element can contain the Engine's projected image under the stated conditions. **❌** means it is not a supported direct destination; select a suitable surrounding container instead. This is a DOM presentation contract, not a promise that the Engine implements that component's native control, interactions or animations. Native capture eligibility is a separate requirement.
|
|
6
|
+
|
|
7
|
+
## Shared requirements
|
|
8
|
+
|
|
9
|
+
- Provide one connected HTML element with a nonzero, unfragmented layout box. Its size, placement and layer belong to the application. For inline hosts, use `display: inline-block` or `block` and give them the intended dimensions.
|
|
10
|
+
- `projectionSelector` uses document CSS selectors (ID, class, attribute or descendant). It must match exactly one element and does not enter Shadow DOM. A missing element can be created later; zero or multiple matches keep Native hidden.
|
|
11
|
+
- The retained image is inserted in the element's open ShadowRoot, or as a light-DOM child otherwise. Closed shadow content must inherit the host's visibility and expose a slot that can display the image.
|
|
12
|
+
- An overlay, collapsed item or inactive page is eligible only while it has a visible layout box. Do not force hidden configuration elements to render just to use them as destinations.
|
|
13
|
+
- Host/ancestor clipping, opacity, transforms and filters still affect the image. The image is noninteractive and freezes the captured appearance. See [projection behavior](projection.md#dom-and-native-surface-requirements) for lifecycle, mutation-observer exclusions and native capture limits.
|
|
14
|
+
|
|
15
|
+
## HTML containers
|
|
16
|
+
|
|
17
|
+
| Container element | Example | Support | Notes |
|
|
18
|
+
| --------------------------------------------------------- | -------------------------------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
19
|
+
| Block container | `div`, `section`, `nav`, `article`, `header`, `footer` | ⭕️ | Give the container the intended size and Web layer. |
|
|
20
|
+
| Inline container | `span`, `a`, `label` | ⭕️ | Use `inline-block` or `block`; wrapped inline text is not a single projection surface. |
|
|
21
|
+
| Button | `button` | ⭕️ | The original button, listeners and values remain; the image does not receive clicks. |
|
|
22
|
+
| Custom element without Shadow DOM | `native-ad` | ⭕️ | Provide a layout box and allow an image child. |
|
|
23
|
+
| Custom element with open Shadow DOM | `native-control` | ⭕️ | The Engine inserts the image directly in the open root and hides fallback content during projection. |
|
|
24
|
+
| Custom element with closed Shadow DOM and a suitable slot | `native-control` with a default slot | ⭕️ | The image must be slotted visibly; internal content must inherit host visibility. The Engine cannot enforce rules inside the closed root. |
|
|
25
|
+
| Custom element with inaccessible rendering content | Closed root without a suitable slot | ❌ | Use an outer container; a light-DOM image would not be rendered. |
|
|
26
|
+
| Native form control | `select`, `input`, `textarea` | ❌ | Direct scopes are rejected. Wrap the control; its value and listeners are preserved. |
|
|
27
|
+
| Replaced media or drawing surface | `img`, `canvas`, `video`, `audio`, `iframe`, `embed`, `object` | ❌ | Direct scopes are rejected. Use a container around the content. |
|
|
28
|
+
| Void element | `br`, `hr`, `area`, `base`, `link`, `meta`, `source`, `track`, `wbr` | ❌ | Direct scopes are rejected; these cannot contain a projected image. |
|
|
29
|
+
| Configuration or special child-rendering element | `option`, `optgroup`, `template` | ❌ | Acceptance by an HTMLElement check does not make an image child renderable. Use a normal container. |
|
|
30
|
+
| Non-HTML element | `svg`, SVG `g`, MathML `math` | ❌ | Use an HTML wrapper, not the SVG/MathML node itself. |
|
|
31
|
+
| Element without a usable box | `display: none`, `display: contents`, zero size | ❌ | Choose a visible container with the intended dimensions. |
|
|
32
|
+
| Pseudo-element | `::before`, `::after` | ❌ | Not DOM elements and cannot be selected as a scope. Both remain available to the application's CSS. |
|
|
33
|
+
|
|
34
|
+
## Ionic elements
|
|
35
|
+
|
|
36
|
+
The following inventory covers every component in the [Ionic API index](https://ionicframework.com/docs/api), with one row per element. It was checked against `@ionic/core` **9.0.9**; `ion-icon` is supplied by Ionicons. The final table separately lists the additional internal component shipped in that version.
|
|
37
|
+
|
|
38
|
+
Ionic custom elements are not the HTML elements with similar names: `ion-select`, `ion-input`, `ion-textarea` and `ion-img` can be image containers, while HTML `select`, `input`, `textarea` and `img` cannot. Every ⭕️ below still requires the shared layout conditions above and the component's normal Ionic parent/context.
|
|
39
|
+
|
|
40
|
+
### Buttons and toolbar
|
|
41
|
+
|
|
42
|
+
| Container element | Example | Support | Notes |
|
|
43
|
+
| ------------------- | ------------------------------- | ------- | ------------------------------------------------------------------------------------ |
|
|
44
|
+
| `ion-button` | `ion-button#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
45
|
+
| `ion-buttons` | `ion-buttons#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
46
|
+
| `ion-back-button` | `ion-back-button#native-slot` | ⭕️ | Requires a back destination or defaultHref to remain visible. |
|
|
47
|
+
| `ion-menu-button` | `ion-menu-button#native-slot` | ⭕️ | Requires an available menu, or autoHide=false. |
|
|
48
|
+
| `ion-toolbar` | `ion-toolbar#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
49
|
+
| `ion-header` | `ion-header#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
50
|
+
| `ion-footer` | `ion-footer#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
51
|
+
| `ion-title` | `ion-title#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
52
|
+
| `ion-ripple-effect` | `ion-ripple-effect#native-slot` | ⭕️ | A captured decorative surface, not a button. Prefer the owning button for a control. |
|
|
53
|
+
|
|
54
|
+
### Forms and selection
|
|
55
|
+
|
|
56
|
+
| Container element | Example | Support | Notes |
|
|
57
|
+
| --------------------------- | --------------------------------------- | ------- | --------------------------------------------------------------------------------------------------- |
|
|
58
|
+
| `ion-checkbox` | `ion-checkbox#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
59
|
+
| `ion-input` | `ion-input#native-slot` | ⭕️ | The host is supported; its internal HTML input is not a destination. |
|
|
60
|
+
| `ion-input-otp` | `ion-input-otp#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
61
|
+
| `ion-input-password-toggle` | `ion-input-password-toggle#native-slot` | ⭕️ | Use inside ion-input; give the host an inline-block box to avoid inline baseline offsets. |
|
|
62
|
+
| `ion-textarea` | `ion-textarea#native-slot` | ⭕️ | The host is supported; its internal HTML textarea is not a destination. |
|
|
63
|
+
| `ion-radio` | `ion-radio#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
64
|
+
| `ion-radio-group` | `ion-radio-group#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
65
|
+
| `ion-range` | `ion-range#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
66
|
+
| `ion-searchbar` | `ion-searchbar#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
67
|
+
| `ion-select` | `ion-select#native-slot` | ⭕️ | The host is supported. Its separately presented options overlay is not part of the host image. |
|
|
68
|
+
| `ion-select-option` | `ion-select-option#native-slot` | ❌ | Configuration for ion-select; no standalone rendered box. Select ion-select instead. |
|
|
69
|
+
| `ion-toggle` | `ion-toggle#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
70
|
+
| `ion-datetime` | `ion-datetime#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
71
|
+
| `ion-datetime-button` | `ion-datetime-button#native-slot` | ⭕️ | Connect the intended ion-datetime; the opened picker is a separate surface. |
|
|
72
|
+
| `ion-picker` | `ion-picker#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
73
|
+
| `ion-picker-column` | `ion-picker-column#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
74
|
+
| `ion-picker-column-option` | `ion-picker-column-option#native-slot` | ⭕️ | A rendered picker option, unlike the non-rendered ion-select-option. Keep its picker-column parent. |
|
|
75
|
+
|
|
76
|
+
### Content and layout
|
|
77
|
+
|
|
78
|
+
| Container element | Example | Support | Notes |
|
|
79
|
+
| ----------------- | ---------------------------- | ------- | ---------------------------------------------------------------------------------------------- |
|
|
80
|
+
| `ion-app` | `ion-app#native-slot` | ⭕️ | Targets the whole app-sized box, not one nested control. |
|
|
81
|
+
| `ion-content` | `ion-content#native-slot` | ⭕️ | Targets the outer content surface; for one scrolled control, select its own container instead. |
|
|
82
|
+
| `ion-grid` | `ion-grid#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
83
|
+
| `ion-row` | `ion-row#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
84
|
+
| `ion-col` | `ion-col#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
85
|
+
| `ion-split-pane` | `ion-split-pane#native-slot` | ⭕️ | Targets the full split layout; select a child container for one pane. |
|
|
86
|
+
|
|
87
|
+
### Cards, text and media
|
|
88
|
+
|
|
89
|
+
| Container element | Example | Support | Notes |
|
|
90
|
+
| ------------------- | ------------------------------- | ------- | ------------------------------------------------------------------------------------- |
|
|
91
|
+
| `ion-card` | `ion-card#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
92
|
+
| `ion-card-content` | `ion-card-content#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
93
|
+
| `ion-card-header` | `ion-card-header#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
94
|
+
| `ion-card-title` | `ion-card-title#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
95
|
+
| `ion-card-subtitle` | `ion-card-subtitle#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
96
|
+
| `ion-text` | `ion-text#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
97
|
+
| `ion-label` | `ion-label#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
98
|
+
| `ion-note` | `ion-note#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
99
|
+
| `ion-badge` | `ion-badge#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
100
|
+
| `ion-chip` | `ion-chip#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
101
|
+
| `ion-avatar` | `ion-avatar#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
102
|
+
| `ion-thumbnail` | `ion-thumbnail#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
103
|
+
| `ion-img` | `ion-img#native-slot` | ⭕️ | Select the Ionic host, not the internal HTML img. Wait for the intended image size. |
|
|
104
|
+
| `ion-icon` | `ion-icon#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
105
|
+
| `ion-skeleton-text` | `ion-skeleton-text#native-slot` | ⭕️ | The image preserves one captured state; it does not continue the indicator animation. |
|
|
106
|
+
| `ion-spinner` | `ion-spinner#native-slot` | ⭕️ | The image preserves one captured state; it does not continue the indicator animation. |
|
|
107
|
+
| `ion-progress-bar` | `ion-progress-bar#native-slot` | ⭕️ | The image preserves one captured state; it does not continue the indicator animation. |
|
|
108
|
+
|
|
109
|
+
### Lists and gestures
|
|
110
|
+
|
|
111
|
+
| Container element | Example | Support | Notes |
|
|
112
|
+
| ----------------------------- | ----------------------------------------- | ------- | --------------------------------------------------------------------------------------- |
|
|
113
|
+
| `ion-accordion` | `ion-accordion#native-slot` | ⭕️ | Targets the current accordion box; expansion changes require a geometry update. |
|
|
114
|
+
| `ion-accordion-group` | `ion-accordion-group#native-slot` | ⭕️ | Targets the group box, including its current expansion state. |
|
|
115
|
+
| `ion-list` | `ion-list#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
116
|
+
| `ion-list-header` | `ion-list-header#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
117
|
+
| `ion-item` | `ion-item#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
118
|
+
| `ion-item-divider` | `ion-item-divider#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
119
|
+
| `ion-item-group` | `ion-item-group#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
120
|
+
| `ion-item-sliding` | `ion-item-sliding#native-slot` | ⭕️ | Targets the whole sliding row; the image does not implement the swipe gesture. |
|
|
121
|
+
| `ion-item-options` | `ion-item-options#native-slot` | ⭕️ | Only while the options are revealed by the parent sliding item. |
|
|
122
|
+
| `ion-item-option` | `ion-item-option#native-slot` | ⭕️ | Only while the parent item-options is revealed. |
|
|
123
|
+
| `ion-infinite-scroll` | `ion-infinite-scroll#native-slot` | ⭕️ | Requires a visible, nonzero box; loading and scroll behavior stay with Ionic. |
|
|
124
|
+
| `ion-infinite-scroll-content` | `ion-infinite-scroll-content#native-slot` | ⭕️ | The image freezes the current loading indicator. |
|
|
125
|
+
| `ion-refresher` | `ion-refresher#native-slot` | ⭕️ | Only in its visible active state inside ion-content; hidden idle state is not eligible. |
|
|
126
|
+
| `ion-refresher-content` | `ion-refresher-content#native-slot` | ⭕️ | Use within an active refresher; the image freezes the current indicator. |
|
|
127
|
+
| `ion-reorder` | `ion-reorder#native-slot` | ⭕️ | Keep the item/reorder-group context; the image does not implement dragging. |
|
|
128
|
+
| `ion-reorder-group` | `ion-reorder-group#native-slot` | ⭕️ | Targets the whole group; ordering and drag behavior remain with Ionic. |
|
|
129
|
+
|
|
130
|
+
### Navigation and tabs
|
|
131
|
+
|
|
132
|
+
| Container element | Example | Support | Notes |
|
|
133
|
+
| --------------------- | --------------------------------- | ------- | -------------------------------------------------------------------------------------------- |
|
|
134
|
+
| `ion-breadcrumb` | `ion-breadcrumb#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
135
|
+
| `ion-breadcrumbs` | `ion-breadcrumbs#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
136
|
+
| `ion-nav` | `ion-nav#native-slot` | ⭕️ | Targets the navigation viewport; use the current page container for a page-specific surface. |
|
|
137
|
+
| `ion-nav-link` | `ion-nav-link#native-slot` | ⭕️ | Give this action wrapper an inline-block/block box. |
|
|
138
|
+
| `ion-router` | `ion-router#native-slot` | ❌ | Routing configuration, not a visible surface. Select the rendered page container. |
|
|
139
|
+
| `ion-router-link` | `ion-router-link#native-slot` | ⭕️ | Give this action wrapper an inline-block/block box. |
|
|
140
|
+
| `ion-router-outlet` | `ion-router-outlet#native-slot` | ⭕️ | Targets the outlet viewport; use the active page container for page-specific placement. |
|
|
141
|
+
| `ion-route` | `ion-route#native-slot` | ❌ | Route definition, not a visible surface. Select the rendered page container. |
|
|
142
|
+
| `ion-route-redirect` | `ion-route-redirect#native-slot` | ❌ | Redirect definition, not a visible surface. Select the rendered page container. |
|
|
143
|
+
| `ion-tabs` | `ion-tabs#native-slot` | ⭕️ | Targets the tabs viewport. Use ion-tab-bar for only the bar. |
|
|
144
|
+
| `ion-tab` | `ion-tab#native-slot` | ⭕️ | Only the active, visible tab is eligible. |
|
|
145
|
+
| `ion-tab-bar` | `ion-tab-bar#native-slot` | ⭕️ | The bar is one surface; use ion-tab-button for individual tabs. |
|
|
146
|
+
| `ion-tab-button` | `ion-tab-button#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
147
|
+
| `ion-segment` | `ion-segment#native-slot` | ⭕️ | The segment is one surface; use ion-segment-button for an individual control. |
|
|
148
|
+
| `ion-segment-button` | `ion-segment-button#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
149
|
+
| `ion-segment-view` | `ion-segment-view#native-slot` | ⭕️ | Targets the view viewport; individual pages use ion-segment-content. |
|
|
150
|
+
| `ion-segment-content` | `ion-segment-content#native-slot` | ⭕️ | Use the visible segment page, not an offscreen page. |
|
|
151
|
+
|
|
152
|
+
### Overlays, menus and FAB
|
|
153
|
+
|
|
154
|
+
| Container element | Example | Support | Notes |
|
|
155
|
+
| ------------------ | ------------------------------ | ------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
156
|
+
| `ion-action-sheet` | `ion-action-sheet#native-slot` | ⭕️ | Only while presented. Targets the overlay host box, not automatically its inner panel; prefer a container inside the overlay for one control. |
|
|
157
|
+
| `ion-alert` | `ion-alert#native-slot` | ⭕️ | Only while presented. Targets the overlay host box, not automatically its inner panel; prefer a container inside the overlay for one control. |
|
|
158
|
+
| `ion-modal` | `ion-modal#native-slot` | ⭕️ | Only while presented. Targets the overlay host box, not automatically its inner panel; prefer a container inside the overlay for one control. |
|
|
159
|
+
| `ion-popover` | `ion-popover#native-slot` | ⭕️ | Only while presented. Targets the overlay host box, not automatically its inner panel; prefer a container inside the overlay for one control. |
|
|
160
|
+
| `ion-loading` | `ion-loading#native-slot` | ⭕️ | Only while presented. Targets the overlay host box, not automatically its inner panel; prefer a container inside the overlay for one control. |
|
|
161
|
+
| `ion-toast` | `ion-toast#native-slot` | ⭕️ | Only while presented. Targets the overlay host box, not automatically its inner panel; prefer a container inside the overlay for one control. |
|
|
162
|
+
| `ion-backdrop` | `ion-backdrop#native-slot` | ⭕️ | A decorative covering surface. This does not configure overlay detection or native z-order. |
|
|
163
|
+
| `ion-menu` | `ion-menu#native-slot` | ⭕️ | Only while presented with a visible box; its open/close behavior remains with Ionic. |
|
|
164
|
+
| `ion-menu-toggle` | `ion-menu-toggle#native-slot` | ⭕️ | Requires visible content; use inline-block/block and account for autoHide. |
|
|
165
|
+
| `ion-fab` | `ion-fab#native-slot` | ⭕️ | Use the visible host with its intended layout box. |
|
|
166
|
+
| `ion-fab-button` | `ion-fab-button#native-slot` | ⭕️ | The button is a surface independently of its parent FAB group. |
|
|
167
|
+
| `ion-fab-list` | `ion-fab-list#native-slot` | ⭕️ | Only while expanded; retain its ion-fab parent and side placement. |
|
|
168
|
+
|
|
169
|
+
### Internal component
|
|
170
|
+
|
|
171
|
+
| Container element | Example | Support | Notes |
|
|
172
|
+
| ------------------ | ------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
173
|
+
| `ion-select-modal` | Internal select overlay content | ❌ | Shipped by Ionic 9.0.9 but not a public destination contract. Select the public ion-select host or an application-owned container. |
|
|
174
|
+
|
|
175
|
+
## Wrapping an unsupported element
|
|
176
|
+
|
|
177
|
+
Select `#seat-projection`, not the HTML `select`:
|
|
178
|
+
|
|
179
|
+
```html
|
|
180
|
+
<span id="seat-projection" style="display: inline-block; width: 12rem; height: 2.75rem">
|
|
181
|
+
<select aria-label="Seat" style="width: 100%; height: 100%">
|
|
182
|
+
<option>Any seat</option>
|
|
183
|
+
</select>
|
|
184
|
+
</span>
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The control's DOM, value and event listeners survive handoff. The plugin still owns native behavior and value synchronization; the prepared image does not accept input.
|
|
188
|
+
|
|
189
|
+
## Verification scope
|
|
190
|
+
|
|
191
|
+
Temporary Chromium and WebKit probes exercise the Ionic inventory with an explicit layout box, required parents and visible states. A solid-color prepared image checks placement and visible pixel coverage; the probes also check original child identity/value preservation and removal of Engine images on teardown. Configuration elements are checked for the absence of a renderable box. The refresher probe selects its active CSS state; it does not validate the pull gesture. Animated loading indicators can change between the before/after captures.
|
|
192
|
+
|
|
193
|
+
In both browsers, all 90 supported public Ionic elements had zero measured image-frame difference, full solid-color coverage in the inspected interior, preserved original children/values and no retained Engine image after destruction. The four public configuration elements had no renderable box. The internal component was also probed but remains outside the supported public contract. HTML probes cover 41 element/layout combinations, including open and closed shadow roots.
|
|
194
|
+
|
|
195
|
+
These are DOM-container checks, not native capture, SDK eligibility or complete Ionic interaction tests. They do not guarantee every theme, closed-shadow implementation or application stylesheet. The probes are not part of the library's permanent test suite.
|
package/docs/projection.md
CHANGED
|
@@ -30,15 +30,19 @@ The Engine owns image readiness and restoration acknowledgement. It must not hid
|
|
|
30
30
|
|
|
31
31
|
Registration and notification forwarding retain the latest intent across startup and reconnection; they do not replay business events. Destruction retires the registration and its cached images. Notifications are intent, not completion promises: an API that promises completed hiding must await the explicit retirement acknowledgement described under DOM and native surface requirements.
|
|
32
32
|
|
|
33
|
-
An existing renderer that checks CSS visibility can call `getUnprojectedVisibility(element)` from the npm package. It reads current authored/inherited visibility without the Engine's hiding rules, and restores projection ownership before returning. Do not
|
|
33
|
+
An existing renderer that checks CSS visibility can call `getUnprojectedVisibility(element)` from the npm package. It reads current authored/inherited visibility without the Engine's hiding rules, and restores projection ownership before returning. Do not use Engine marker attributes to infer projection ownership or mutate them in a consuming plugin. Excluding Engine-owned DOM mutations from renderer invalidation is described below. This helper performs a style read and can trigger attribute observers; it does not eliminate measurement or transport costs.
|
|
34
34
|
|
|
35
35
|
## DOM and native surface requirements
|
|
36
36
|
|
|
37
|
-
`projectionSelector` is required and must resolve to **one existing container when projection starts**,
|
|
37
|
+
`projectionSelector` is required and must resolve to **one existing container when projection starts**, using a valid CSS selector. ID, class, attribute and descendant selectors are supported; zero or multiple matches leave Native hidden. The engine does not create the fallback container or choose its size, placement, layer or z-index. Keep the container's normal layout and any interactive Web controls; an empty zero-size placeholder is not a usable fallback. It is a fixed scope, not a discovery root.
|
|
38
38
|
|
|
39
39
|
The container does not have to exist at registration time. A missing DOM waits without showing native content; replacing it creates a new binding and retires old images. The engine preserves the container's children, listeners and values. Pass the native view itself through `view`; a group of controls can use their common native container. The engine manages the internal identifier, so no ID list or view dictionary is required. Independent surfaces use separate registrations.
|
|
40
40
|
|
|
41
|
-
A Web Component host can be the destination, for example `projectionSelector: 'native-ad[placement="home"]'`. Give the host a layout box (`display: block` with the intended dimensions); the engine observes insertion, removal and selector attribute changes. The selector runs in the document and does not search inside shadow roots. The fallback content
|
|
41
|
+
A Web Component host can be the destination, for example `projectionSelector: 'native-ad[placement="home"]'`. Give the host a layout box (`display: block` with the intended dimensions); the engine observes insertion, removal and selector attribute changes. The selector runs in the document and does not search inside shadow roots. The engine suppresses fallback content and pseudo-elements in the light DOM and open shadow roots during projection. Closed shadow content cannot be inspected and must inherit the host's `visibility`; provide a container whose shadow/slot structure can display the inserted image.
|
|
42
|
+
|
|
43
|
+
See [Projection container support](projection-containers.md) for the HTML and complete Ionic element tables, sizing conditions and a wrapper example.
|
|
44
|
+
|
|
45
|
+
### Native surface requirements
|
|
42
46
|
|
|
43
47
|
For independently moving AdMob slots, retain one registration per loaded native view/surface, use a slot-specific selector and filter the plugin's notifications through `receiveListeners.match` (`match: ["slotKey": slotKey, "loadId": loadId]` / `match = mapOf("slotKey" to slotKey, "loadId" to loadId)`). Dispose the registration when a loaded ad is replaced, even if its host view is reused. Keep SDK loading, impression and click handling in the ad plugin. Supply `canCapture` with its existing capture eligibility checks (for example, test-ad / opaque-background / no-video restrictions); the engine does not grant permission to snapshot an SDK view.
|
|
44
48
|
|
|
@@ -178,7 +182,13 @@ The container, bridge and update callbacks above belong to the consuming plugin.
|
|
|
178
182
|
5. Call `projection.end()`, send the normal native update, then call `projection.updated(snapshotToken, acceptedIDs, revision)`. A newer acknowledgement containing all captured IDs retires the image.
|
|
179
183
|
6. Call `projection.destroy()` when disposing the surface. It releases only its own images and listener; the plugin owns final Native reset and its DOM.
|
|
180
184
|
|
|
181
|
-
The engine
|
|
185
|
+
The engine prepares a noninteractive `img` inside the fallback container (directly inside its open ShadowRoot when present) while Native is visible. It reuses the decoded image and DOM node across handoffs, and releases them when the scope is retired or destroyed. The image is `inert`, hidden from accessibility and excluded from hit testing. The existing fallback and its children are never cloned, replaced, removed or assigned `inert`/`aria-hidden`; their listeners, values and application changes survive projection. Both `::before` and `::after` remain available to the application.
|
|
186
|
+
|
|
187
|
+
Supply an existing container such as `div`, `button` or a custom element. Replaced/void elements such as `img`, `input` and `video` remain unsupported as scopes; use their existing fallback container. The renderer may move the same container between parents, but must not replace a low-level scope's element or let two managers control it simultaneously. Notify the manager through `moved()` when changing its placement during projection.
|
|
188
|
+
|
|
189
|
+
The image is absolutely positioned within the fallback, which retains its layout box. As with the previous pseudo-element presentation, static hosts become positioned while the image is visible. Host and ancestor clipping, transforms and filters still apply. Open ShadowRoot insertion bypasses internal slot wrappers, not the host's own clipping. Closed shadow roots cannot be inspected; provide a fallback container that can render the image.
|
|
190
|
+
|
|
191
|
+
Engine-owned nodes carry `data-native-ui-projection-node` for their entire pooled lifetime. Renderers that inspect fallback children must exclude these nodes from application content discovery. A renderer that uses a MutationObserver to invalidate captures must also ignore insertion/removal and attribute changes of these nodes, changes inside them, and Engine-owned `data-native-ui-snapshot-*` attributes. Continue observing authored content, dimensions and disabled-state changes; ignoring all mutations on a projection host would hide real updates. Otherwise image preparation can invalidate itself and cause repeated capture/release cycles. The engine removes only its own nodes on disposal. Child insertion can affect structural selectors within the fallback, but does not add items alongside it in flex, grid or virtual lists.
|
|
182
192
|
|
|
183
193
|
### Notifications from the existing plugin
|
|
184
194
|
|
|
@@ -210,7 +220,7 @@ Use `createWebProjectionBatch(transport, options)` when several fallback element
|
|
|
210
220
|
|
|
211
221
|
The batch derives its Document from the first nonempty `consider`; all subsequent scopes and sources must belong to that Document. Before this first update, `begin()` does nothing and `destroy()` is safe. A changed fallback requires a new scope. Scope identity participates in cache invalidation; an active presentation retains its captured scopes until restoration finishes.
|
|
212
222
|
|
|
213
|
-
`WebProjectionRequest` is an action-discriminated union. `prepare` requires `revision`, `viewportWidth` and `ids`; `cover` requires `token
|
|
223
|
+
`WebProjectionRequest` is an action-discriminated union. `prepare` requires `revision`, `viewportWidth` and `ids`; `cover` requires `token` and includes `viewportWidth` and destination `parts` for transient movement; `restore` requires `token`, `viewportWidth` and destination `parts`; `painted`, `restored` and `release` require `token`; `reset` takes no payload. Empty preparation results and `{ shown: false }` mean the plugin should use its normal Web fallback. Use the plugin's existing ordered bridge; the engine does not retry user actions.
|
|
214
224
|
|
|
215
225
|
### Native handoff on both platforms
|
|
216
226
|
|
|
@@ -219,13 +229,17 @@ Call all view and snapshot lifecycle APIs on the main/UI thread. Native input fr
|
|
|
219
229
|
On both platforms, `cover` installs the prepared entry image **before hiding the supplied live views**, within the same UI turn. Its boolean result reports whether this was possible; failure leaves visibility unchanged. Supply the actual views for every captured component. Do not separately hide views or create another entry copy.
|
|
220
230
|
|
|
221
231
|
```swift
|
|
222
|
-
let shown = snapshots.cover(token, in: webView, views: renderedViews, transient: movingSurface)
|
|
232
|
+
let shown = snapshots.cover(token, in: webView, views: renderedViews, transient: movingSurface, frames: destinationFrames)
|
|
223
233
|
```
|
|
224
234
|
|
|
225
235
|
```kotlin
|
|
226
|
-
val shown = snapshots.cover(token, activity.window, webView, renderedViews, transient = movingSurface)
|
|
236
|
+
val shown = snapshots.cover(token, activity.window, webView, renderedViews, transient = movingSurface, frames = destinationFrames)
|
|
227
237
|
```
|
|
228
238
|
|
|
239
|
+
For a custom transport, convert request `parts` from CSS pixels to native frames using `webView.bounds.width / viewportWidth` on iOS or `webView.width / viewportWidth` on Android, mapping each part's IDs to its rectangle. Pass these as `destinationFrames` for both `cover` and `restore`. A retained image can move before its first cover or after a failed return copy; always use the supplied coordinates. When `parts` is absent, pass `nil` / `null` to retain the captured placement. The high-level `NativeUIProjection` registration handles this conversion automatically.
|
|
240
|
+
|
|
241
|
+
A transient surface keeps its image while its sources retain the same sizes and positions relative to their projection scopes. Moving the whole scope can reuse pixels; moving a source inside a fixed scope invalidates the cached layout.
|
|
242
|
+
|
|
229
243
|
`transient: true` is for a moving foreground surface: it does not replace the background's `coveringToken`. Retire the entry copy with `retireCopy(token)` after Web image readiness. Before applying a normal native update, undo the engine's visibility changes with iOS `restoreControls()` / Android `restoreViews()`. For return, `restore` installs the cached return image, the plugin renders underneath it, and `retireCopy(token, entry: false)` retires it after the corresponding acknowledgement/readiness. These platform-native names remain idiomatic; the lifecycle is the same. `cover` is an entry operation, not a method to repeat on each normal update.
|
|
230
244
|
|
|
231
245
|
Token cleanup uses `release(token)`; owner teardown uses `reset()` (and Android `close()` when disposing its encoder). A surface manager must not reset another surface's captures. Web fallback remains the renderer's responsibility when a capture or native placement is unavailable.
|
|
@@ -234,15 +248,15 @@ The optional `eligible(source)` and `current(sources)` callbacks keep framework-
|
|
|
234
248
|
|
|
235
249
|
The image transport accepts `WebProjectionRequest` and returns prepared token/parts or `{ shown: false }`. Parts use local `file:` URLs and CSS-pixel viewport coordinates. Requests use the plugin's existing ordered bridge:
|
|
236
250
|
|
|
237
|
-
| Action | Native responsibility
|
|
238
|
-
| ---------- |
|
|
239
|
-
| `prepare` | Validate the current revision and IDs, then `NativeUICoveredSnapshots.prepare` using actual rendered frames.
|
|
240
|
-
| `cover` | Validate the token and complete native surface ownership; create the entry copy and hide the associated native views. Return `shown: false` on failure. |
|
|
241
|
-
| `painted` | Retire only that token's entry copy.
|
|
242
|
-
| `restore` | Rebase the prepared return copy to supplied frames. Never capture here.
|
|
243
|
-
| `restored` | Retire only that token's return copy after the native update ACK and Web paint boundary.
|
|
244
|
-
| `release` | Release the named token, files and associated temporary views.
|
|
245
|
-
| `reset` | Owner teardown only; release all native capture resources.
|
|
251
|
+
| Action | Native responsibility |
|
|
252
|
+
| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
253
|
+
| `prepare` | Validate the current revision and IDs, then `NativeUICoveredSnapshots.prepare` using actual rendered frames. |
|
|
254
|
+
| `cover` | Validate the token and complete native surface ownership; create the entry copy at supplied frames (or captured frames when absent) and hide the associated native views. Return `shown: false` on failure. |
|
|
255
|
+
| `painted` | Retire only that token's entry copy. |
|
|
256
|
+
| `restore` | Rebase the prepared return copy to supplied frames. Never capture here. |
|
|
257
|
+
| `restored` | Retire only that token's return copy after the native update ACK and Web paint boundary. |
|
|
258
|
+
| `release` | Release the named token, files and associated temporary views. |
|
|
259
|
+
| `reset` | Owner teardown only; release all native capture resources. |
|
|
246
260
|
|
|
247
261
|
Use `NativeUICoveredSnapshots.observe` to notify the JS listener when interaction starts/ends. On iOS, captures rasterize the visible window with `drawHierarchy(in:afterScreenUpdates:)`, including the background under the clipped native component. Screen snapshot views are display replicas and are not used as bitmap sources. UIKit rasterization can differ from the live compositor for materials such as Liquid Glass; validate the appearance on physical devices with the backgrounds your plugin supports. Cache validity therefore includes layout and the plugin's visual-change notifications. The engine does not capture continuously or guarantee every possible renderer can be captured.
|
|
248
262
|
|
|
@@ -150,8 +150,8 @@ public final class NativeUICoveredSnapshots {
|
|
|
150
150
|
|
|
151
151
|
/// Install prepared pixels before hiding live views, within the same main-thread turn.
|
|
152
152
|
/// A transient (moving) surface does not replace background covering ownership.
|
|
153
|
-
@discardableResult public func cover(_ token: String, in webView: UIView, views: [UIView], transient: Bool = false) -> Bool {
|
|
154
|
-
guard restore(in: webView, token: token, entry: true) else { return false }
|
|
153
|
+
@discardableResult public func cover(_ token: String, in webView: UIView, views: [UIView], transient: Bool = false, frames: [String: CGRect]? = nil) -> Bool {
|
|
154
|
+
guard restore(in: webView, token: token, frames: frames, entry: true) else { return false }
|
|
155
155
|
if !transient { coveringToken = token }
|
|
156
156
|
hide(views, for: token)
|
|
157
157
|
return true
|
|
@@ -30,7 +30,18 @@ public final class NativeUIProjection: NSObject, WKScriptMessageHandler {
|
|
|
30
30
|
private let views: () -> [String: UIView]
|
|
31
31
|
private let snapshots: NativeUICoveredSnapshots
|
|
32
32
|
private let scrolling = NativeUIScrollBindings()
|
|
33
|
-
private
|
|
33
|
+
private struct ScrollMask {
|
|
34
|
+
weak var view: UIView?
|
|
35
|
+
let layer: CAShapeLayer
|
|
36
|
+
}
|
|
37
|
+
private var masks: [String: ScrollMask] = [:]
|
|
38
|
+
private func resetScrolling() {
|
|
39
|
+
scrolling.reset()
|
|
40
|
+
for mask in masks.values where mask.view?.layer.mask === mask.layer {
|
|
41
|
+
mask.view?.layer.mask = nil
|
|
42
|
+
}
|
|
43
|
+
masks.removeAll()
|
|
44
|
+
}
|
|
34
45
|
private let id = "nativeUI" + UUID().uuidString.replacingOccurrences(of: "-", with: "")
|
|
35
46
|
private let origins: Set<String>
|
|
36
47
|
private let allowedOriginsJSON: String
|
|
@@ -145,12 +156,26 @@ public final class NativeUIProjection: NSObject, WKScriptMessageHandler {
|
|
|
145
156
|
let key = message["key"] as? String, key.hasPrefix(id + ":"),
|
|
146
157
|
let request = message["request"] as? [String: Any] else { return }
|
|
147
158
|
let page = key.split(separator: ":").dropFirst().first.map(String.init)
|
|
148
|
-
if documentID != page { snapshots.reset();
|
|
159
|
+
if documentID != page { snapshots.reset(); resetScrolling(); captured.removeAll(); anchors.removeAll(); documentID = page }
|
|
149
160
|
let complete: ([String: Any]) -> Void = { [weak self] value in
|
|
150
161
|
guard let self, !self.closed, self.documentID == page else { return }
|
|
151
162
|
self.reply(key, value)
|
|
152
163
|
}
|
|
153
164
|
let token = request["token"] as? String ?? ""
|
|
165
|
+
func projectionFrames() -> [String: CGRect]? {
|
|
166
|
+
guard let width = request["viewportWidth"] as? Double, width.isFinite, width > 0,
|
|
167
|
+
let parts = request["parts"] as? [[String: Any]] else { return nil }
|
|
168
|
+
var frames: [String: CGRect] = [:]
|
|
169
|
+
let scale = webView.bounds.width / width
|
|
170
|
+
for part in parts {
|
|
171
|
+
guard let ids = part["ids"] as? [String], let x = part["x"] as? Double, let y = part["y"] as? Double,
|
|
172
|
+
let w = part["width"] as? Double, let h = part["height"] as? Double,
|
|
173
|
+
[x,y,w,h].allSatisfy({ $0.isFinite }), w > 0, h > 0 else { return nil }
|
|
174
|
+
for id in ids { frames[id] = CGRect(x: x*scale, y: y*scale, width: w*scale, height: h*scale) }
|
|
175
|
+
}
|
|
176
|
+
guard Set(frames.keys) == snapshots.identifiers(for: token), !frames.isEmpty else { return nil }
|
|
177
|
+
return frames
|
|
178
|
+
}
|
|
154
179
|
switch request["action"] as? String {
|
|
155
180
|
case "connect":
|
|
156
181
|
if !connected { destination = (views().values.contains(where: { !$0.isHidden }) ? "native" : "none"); connected = true }
|
|
@@ -159,9 +184,7 @@ public final class NativeUIProjection: NSObject, WKScriptMessageHandler {
|
|
|
159
184
|
if let next = request["destination"] as? String, ["native", "web", "none"].contains(next) { destination = next; connected = true }
|
|
160
185
|
complete([:])
|
|
161
186
|
case "track":
|
|
162
|
-
|
|
163
|
-
for (id, mask) in masks { if views()[id]?.layer.mask === mask { views()[id]?.layer.mask = nil } }
|
|
164
|
-
masks.removeAll()
|
|
187
|
+
resetScrolling()
|
|
165
188
|
guard let raw = request["scroll"] as? [String: Any], let width = request["viewportWidth"] as? Double, width.isFinite, width > 0,
|
|
166
189
|
let data = try? JSONSerialization.data(withJSONObject: raw),
|
|
167
190
|
let placement = try? JSONDecoder().decode(NativeUIScrollPlacement.self, from: data), placement.isValid else {
|
|
@@ -185,8 +208,8 @@ public final class NativeUIProjection: NSObject, WKScriptMessageHandler {
|
|
|
185
208
|
let from = parent.convert(CGPoint(x: old.midX, y: old.midY), from: webView)
|
|
186
209
|
let to = parent.convert(CGPoint(x: frame.midX, y: frame.midY), from: webView)
|
|
187
210
|
view.transform.tx += to.x-from.x; view.transform.ty += to.y-from.y
|
|
188
|
-
let mask = self.masks[id] ?? CAShapeLayer()
|
|
189
|
-
self.masks[id] = mask
|
|
211
|
+
let mask = self.masks[id]?.layer ?? CAShapeLayer()
|
|
212
|
+
self.masks[id] = ScrollMask(view: view, layer: mask)
|
|
190
213
|
mask.frame = view.bounds
|
|
191
214
|
mask.path = UIBezierPath(rect: clip.isNull ? .zero : view.convert(clip, from: webView).intersection(view.bounds)).cgPath
|
|
192
215
|
view.layer.mask = mask
|
|
@@ -221,21 +244,13 @@ public final class NativeUIProjection: NSObject, WKScriptMessageHandler {
|
|
|
221
244
|
case "cover":
|
|
222
245
|
let selected = captured[token] ?? [:], current = views()
|
|
223
246
|
let valid = visible && canCapture() && !selected.isEmpty && selected.allSatisfy { current[$0.key] === $0.value }
|
|
247
|
+
let frames = request["parts"] == nil ? nil : projectionFrames()
|
|
248
|
+
guard request["parts"] == nil || frames != nil else { complete(["shown": false]); return }
|
|
224
249
|
restoreAccepted = true
|
|
225
|
-
complete(["shown": valid && snapshots.cover(token, in: webView, views: Array(Dictionary(grouping: selected.values, by: { ObjectIdentifier($0) }).values.compactMap { $0.first }), transient: true)])
|
|
250
|
+
complete(["shown": valid && snapshots.cover(token, in: webView, views: Array(Dictionary(grouping: selected.values, by: { ObjectIdentifier($0) }).values.compactMap { $0.first }), transient: true, frames: frames)])
|
|
226
251
|
case "restore":
|
|
227
252
|
restoreAccepted = false
|
|
228
|
-
guard let
|
|
229
|
-
let parts = request["parts"] as? [[String: Any]] else { complete(["shown": false]); return }
|
|
230
|
-
var frames: [String: CGRect] = [:]
|
|
231
|
-
let scale = webView.bounds.width / width
|
|
232
|
-
for part in parts {
|
|
233
|
-
guard let ids = part["ids"] as? [String], let x = part["x"] as? Double, let y = part["y"] as? Double,
|
|
234
|
-
let w = part["width"] as? Double, let h = part["height"] as? Double,
|
|
235
|
-
[x,y,w,h].allSatisfy({ $0.isFinite }), w > 0, h > 0 else { complete(["shown": false]); return }
|
|
236
|
-
for id in ids { frames[id] = CGRect(x: x*scale, y: y*scale, width: w*scale, height: h*scale) }
|
|
237
|
-
}
|
|
238
|
-
guard Set(frames.keys) == snapshots.identifiers(for: token), !frames.isEmpty else { complete(["shown": false]); return }
|
|
253
|
+
guard let frames = projectionFrames() else { complete(["shown": false]); return }
|
|
239
254
|
let current = views()
|
|
240
255
|
// Preserve renderer-owned constraints. Translation follows the corresponding DOM's movement.
|
|
241
256
|
for (id, frame) in frames {
|
|
@@ -298,11 +313,9 @@ public final class NativeUIProjection: NSObject, WKScriptMessageHandler {
|
|
|
298
313
|
controller.removeAllUserScripts()
|
|
299
314
|
remaining.forEach { controller.addUserScript($0) }
|
|
300
315
|
}
|
|
301
|
-
snapshots.reset();
|
|
316
|
+
snapshots.reset(); resetScrolling(); captured.removeAll(); anchors.removeAll()
|
|
302
317
|
if destination != "none" { hiddenByProjection.forEach { $0.isHidden = false } }
|
|
303
318
|
hiddenByProjection.removeAll()
|
|
304
|
-
for (id, mask) in masks { if views()[id]?.layer.mask === mask { views()[id]?.layer.mask = nil } }
|
|
305
|
-
masks.removeAll()
|
|
306
319
|
}
|
|
307
320
|
}
|
|
308
321
|
private final class WeakProjectionHandler: NSObject, WKScriptMessageHandler {
|