@rdlabo/capacitor-native-ui 0.0.0-stage → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/Package.swift +23 -0
- package/README.md +53 -2
- package/RdlaboCapacitorNativeUi.podspec +17 -0
- package/android/build.gradle +69 -0
- package/android/proguard-rules.pro +0 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/dev/rdlabo/capacitor/nativeui/NativeUI.kt +51 -0
- package/android/src/main/java/dev/rdlabo/capacitor/nativeui/NativeUIPlugin.kt +192 -0
- package/android/src/main/java/dev/rdlabo/capacitor/nativeui/NativeUISnapshot.kt +57 -0
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/core.d.ts +4 -0
- package/dist/core.js +3 -0
- package/dist/native/activation.d.ts +14 -0
- package/dist/native/activation.d.ts.map +1 -0
- package/dist/native/activation.js +305 -0
- package/dist/native/activation.js.map +1 -0
- package/dist/native/components/index.d.ts +19 -0
- package/dist/native/components/index.d.ts.map +1 -0
- package/dist/native/components/index.js +38 -0
- package/dist/native/components/index.js.map +1 -0
- package/dist/native/components/ion-back-button.d.ts +10 -0
- package/dist/native/components/ion-back-button.d.ts.map +1 -0
- package/dist/native/components/ion-back-button.js +21 -0
- package/dist/native/components/ion-back-button.js.map +1 -0
- package/dist/native/components/ion-button.d.ts +10 -0
- package/dist/native/components/ion-button.d.ts.map +1 -0
- package/dist/native/components/ion-button.js +25 -0
- package/dist/native/components/ion-button.js.map +1 -0
- package/dist/native/components/ion-buttons.d.ts +11 -0
- package/dist/native/components/ion-buttons.d.ts.map +1 -0
- package/dist/native/components/ion-buttons.js +30 -0
- package/dist/native/components/ion-buttons.js.map +1 -0
- package/dist/native/components/ion-fab.d.ts +10 -0
- package/dist/native/components/ion-fab.d.ts.map +1 -0
- package/dist/native/components/ion-fab.js +132 -0
- package/dist/native/components/ion-fab.js.map +1 -0
- package/dist/native/components/ion-menu-button.d.ts +13 -0
- package/dist/native/components/ion-menu-button.d.ts.map +1 -0
- package/dist/native/components/ion-menu-button.js +44 -0
- package/dist/native/components/ion-menu-button.js.map +1 -0
- package/dist/native/components/ion-segment.d.ts +11 -0
- package/dist/native/components/ion-segment.d.ts.map +1 -0
- package/dist/native/components/ion-segment.js +30 -0
- package/dist/native/components/ion-segment.js.map +1 -0
- package/dist/native/components/ion-tab-bar.d.ts +11 -0
- package/dist/native/components/ion-tab-bar.d.ts.map +1 -0
- package/dist/native/components/ion-tab-bar.js +50 -0
- package/dist/native/components/ion-tab-bar.js.map +1 -0
- package/dist/native/components/searchable-tabs.d.ts +20 -0
- package/dist/native/components/searchable-tabs.d.ts.map +1 -0
- package/dist/native/components/searchable-tabs.js +413 -0
- package/dist/native/components/searchable-tabs.js.map +1 -0
- package/dist/native/definitions.d.ts +255 -0
- package/dist/native/definitions.d.ts.map +1 -0
- package/dist/native/definitions.js +0 -0
- package/dist/native/extensions.d.ts +31 -0
- package/dist/native/extensions.d.ts.map +1 -0
- package/dist/native/extensions.js +122 -0
- package/dist/native/extensions.js.map +1 -0
- package/dist/native/foldable/fold-state.d.ts +5 -0
- package/dist/native/foldable/fold-state.d.ts.map +1 -0
- package/dist/native/foldable/fold-state.js +15 -0
- package/dist/native/foldable/fold-state.js.map +1 -0
- package/dist/native/foldable/types.d.ts +12 -0
- package/dist/native/foldable/types.d.ts.map +1 -0
- package/dist/native/foldable/types.js +0 -0
- package/dist/native/html.d.ts +9 -0
- package/dist/native/html.d.ts.map +1 -0
- package/dist/native/html.js +188 -0
- package/dist/native/html.js.map +1 -0
- package/dist/native/index.d.ts +10 -0
- package/dist/native/index.d.ts.map +1 -0
- package/dist/native/index.js +28 -0
- package/dist/native/index.js.map +1 -0
- package/dist/native/legacy-layout.d.ts +5 -0
- package/dist/native/legacy-layout.d.ts.map +1 -0
- package/dist/native/legacy-layout.js +27 -0
- package/dist/native/legacy-layout.js.map +1 -0
- package/dist/native/overlays/content.d.ts +4 -0
- package/dist/native/overlays/content.d.ts.map +1 -0
- package/dist/native/overlays/content.js +51 -0
- package/dist/native/overlays/content.js.map +1 -0
- package/dist/native/overlays/controller.d.ts +8 -0
- package/dist/native/overlays/controller.d.ts.map +1 -0
- package/dist/native/overlays/controller.js +285 -0
- package/dist/native/overlays/controller.js.map +1 -0
- package/dist/native/overlays/dialog-animation.d.ts +9 -0
- package/dist/native/overlays/dialog-animation.d.ts.map +1 -0
- package/dist/native/overlays/dialog-animation.js +53 -0
- package/dist/native/overlays/dialog-animation.js.map +1 -0
- package/dist/native/overlays/dismiss-animation.d.ts +4 -0
- package/dist/native/overlays/dismiss-animation.d.ts.map +1 -0
- package/dist/native/overlays/dismiss-animation.js +18 -0
- package/dist/native/overlays/dismiss-animation.js.map +1 -0
- package/dist/native/overlays/focus.d.ts +5 -0
- package/dist/native/overlays/focus.d.ts.map +1 -0
- package/dist/native/overlays/focus.js +59 -0
- package/dist/native/overlays/focus.js.map +1 -0
- package/dist/native/overlays/popover.d.ts +12 -0
- package/dist/native/overlays/popover.d.ts.map +1 -0
- package/dist/native/overlays/popover.js +110 -0
- package/dist/native/overlays/popover.js.map +1 -0
- package/dist/native/overlays/styles.d.ts +11 -0
- package/dist/native/overlays/styles.d.ts.map +1 -0
- package/dist/native/overlays/styles.js +167 -0
- package/dist/native/overlays/styles.js.map +1 -0
- package/dist/native/prehide.d.ts +8 -0
- package/dist/native/prehide.d.ts.map +1 -0
- package/dist/native/prehide.js +238 -0
- package/dist/native/prehide.js.map +1 -0
- package/dist/native/runtime.d.ts +7 -0
- package/dist/native/runtime.d.ts.map +1 -0
- package/dist/native/runtime.js +788 -0
- package/dist/native/runtime.js.map +1 -0
- package/dist/native/shared/bridge.d.ts +6 -0
- package/dist/native/shared/bridge.d.ts.map +1 -0
- package/dist/native/shared/bridge.js +18 -0
- package/dist/native/shared/bridge.js.map +1 -0
- package/dist/native/shared/candidate.d.ts +25 -0
- package/dist/native/shared/candidate.d.ts.map +1 -0
- package/dist/native/shared/candidate.js +106 -0
- package/dist/native/shared/candidate.js.map +1 -0
- package/dist/native/shared/clipping.d.ts +5 -0
- package/dist/native/shared/clipping.d.ts.map +1 -0
- package/dist/native/shared/clipping.js +26 -0
- package/dist/native/shared/clipping.js.map +1 -0
- package/dist/native/shared/crossfade.d.ts +12 -0
- package/dist/native/shared/crossfade.d.ts.map +1 -0
- package/dist/native/shared/crossfade.js +67 -0
- package/dist/native/shared/crossfade.js.map +1 -0
- package/dist/native/shared/diagnostics.d.ts +4 -0
- package/dist/native/shared/diagnostics.d.ts.map +1 -0
- package/dist/native/shared/diagnostics.js +18 -0
- package/dist/native/shared/diagnostics.js.map +1 -0
- package/dist/native/shared/dom.d.ts +46 -0
- package/dist/native/shared/dom.d.ts.map +1 -0
- package/dist/native/shared/dom.js +180 -0
- package/dist/native/shared/dom.js.map +1 -0
- package/dist/native/shared/events.d.ts +15 -0
- package/dist/native/shared/events.d.ts.map +1 -0
- package/dist/native/shared/events.js +17 -0
- package/dist/native/shared/events.js.map +1 -0
- package/dist/native/shared/icons.d.ts +8 -0
- package/dist/native/shared/icons.d.ts.map +1 -0
- package/dist/native/shared/icons.js +61 -0
- package/dist/native/shared/icons.js.map +1 -0
- package/dist/native/shared/modal.d.ts +10 -0
- package/dist/native/shared/modal.d.ts.map +1 -0
- package/dist/native/shared/modal.js +160 -0
- package/dist/native/shared/modal.js.map +1 -0
- package/dist/native/shared/projection-id.d.ts +4 -0
- package/dist/native/shared/projection-id.d.ts.map +1 -0
- package/dist/native/shared/projection-id.js +7 -0
- package/dist/native/shared/projection-id.js.map +1 -0
- package/dist/native/shared/toolbar-options.d.ts +9 -0
- package/dist/native/shared/toolbar-options.d.ts.map +1 -0
- package/dist/native/shared/toolbar-options.js +36 -0
- package/dist/native/shared/toolbar-options.js.map +1 -0
- package/dist/native/shared/vertical-bars-geometry.d.ts +10 -0
- package/dist/native/shared/vertical-bars-geometry.d.ts.map +1 -0
- package/dist/native/shared/vertical-bars-geometry.js +85 -0
- package/dist/native/shared/vertical-bars-geometry.js.map +1 -0
- package/dist/native/startup.d.ts +8 -0
- package/dist/native/startup.d.ts.map +1 -0
- package/dist/native/startup.js +86 -0
- package/dist/native/startup.js.map +1 -0
- package/dist/native/vertical-bars-web-search.d.ts +10 -0
- package/dist/native/vertical-bars-web-search.d.ts.map +1 -0
- package/dist/native/vertical-bars-web-search.js +90 -0
- package/dist/native/vertical-bars-web-search.js.map +1 -0
- package/dist/native/vertical-bars-web.d.ts +5 -0
- package/dist/native/vertical-bars-web.d.ts.map +1 -0
- package/dist/native/vertical-bars-web.js +421 -0
- package/dist/native/vertical-bars-web.js.map +1 -0
- package/dist/native-integration/index.d.ts +25 -0
- package/dist/native-integration/index.d.ts.map +1 -0
- package/dist/native-integration/index.js +33 -0
- package/dist/native-integration/index.js.map +1 -0
- package/dist/vertical-bars.d.ts +6 -0
- package/dist/vertical-bars.js +5 -0
- package/docs/configuration.md +187 -0
- package/docs/framework-independent.md +82 -0
- package/docs/migration.md +206 -0
- package/docs/native-mini-player.md +47 -0
- package/docs/native-ui-extensions.md +365 -0
- package/docs/native-ui.md +409 -0
- package/docs/setup.md +205 -0
- package/docs/vertical-bars.md +379 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIButton.swift +71 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIButtons.swift +24 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIComponents.swift +19 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIFab.swift +80 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUISearchController.swift +439 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUISegment.swift +91 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUITabBar.swift +218 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIVerticalBars.swift +590 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIVerticalSearch.swift +118 -0
- package/ios/Sources/NativeUIPlugin/Extensions/NativeUI.swift +178 -0
- package/ios/Sources/NativeUIPlugin/Extensions/NativeUIRendering.swift +38 -0
- package/ios/Sources/NativeUIPlugin/NativeUIPlugin.swift +615 -0
- package/ios/Sources/NativeUIPlugin/Overlays/NativeUIOverlayController.swift +119 -0
- package/ios/Sources/NativeUIPlugin/Overlays/NativeUIOverlayHost.swift +75 -0
- package/ios/Sources/NativeUIPlugin/Shared/NativeUIControlRendering.swift +21 -0
- package/ios/Sources/NativeUIPlugin/Shared/NativeUICrossfade.swift +35 -0
- package/ios/Sources/NativeUIPlugin/Shared/NativeUIHost.swift +8 -0
- package/ios/Sources/NativeUIPlugin/Shared/NativeUISnapshot.swift +161 -0
- package/ios/Sources/NativeUIPlugin/Shared/NativeUIVerticalBarsGeometry.swift +52 -0
- package/ios/Tests/NativeUIPluginTests/NativeUIExtensionTests.swift +165 -0
- package/ios/Tests/NativeUIPluginTests/NativeUISnapshotTests.swift +805 -0
- package/package.json +89 -4
- package/src/core.ts +15 -0
- package/src/native/activation.ts +394 -0
- package/src/native/components/index.ts +45 -0
- package/src/native/components/ion-back-button.ts +22 -0
- package/src/native/components/ion-button.ts +25 -0
- package/src/native/components/ion-buttons.ts +37 -0
- package/src/native/components/ion-fab.ts +151 -0
- package/src/native/components/ion-menu-button.ts +37 -0
- package/src/native/components/ion-segment.ts +27 -0
- package/src/native/components/ion-tab-bar.ts +50 -0
- package/src/native/components/searchable-tabs.ts +499 -0
- package/src/native/definitions.ts +324 -0
- package/src/native/extensions.ts +176 -0
- package/src/native/foldable/fold-state.ts +10 -0
- package/src/native/foldable/types.ts +4 -0
- package/src/native/html.ts +193 -0
- package/src/native/index.ts +41 -0
- package/src/native/legacy-layout.ts +21 -0
- package/src/native/overlays/content.ts +51 -0
- package/src/native/overlays/controller.ts +349 -0
- package/src/native/overlays/dialog-animation.ts +53 -0
- package/src/native/overlays/dismiss-animation.ts +15 -0
- package/src/native/overlays/focus.ts +64 -0
- package/src/native/overlays/popover.ts +120 -0
- package/src/native/overlays/styles.ts +181 -0
- package/src/native/prehide.ts +285 -0
- package/src/native/runtime.ts +988 -0
- package/src/native/shared/bridge.ts +18 -0
- package/src/native/shared/candidate.ts +162 -0
- package/src/native/shared/clipping.ts +24 -0
- package/src/native/shared/crossfade.ts +67 -0
- package/src/native/shared/diagnostics.ts +13 -0
- package/src/native/shared/dom.ts +304 -0
- package/src/native/shared/events.ts +14 -0
- package/src/native/shared/icons.ts +57 -0
- package/src/native/shared/modal.ts +157 -0
- package/src/native/shared/projection-id.ts +2 -0
- package/src/native/shared/toolbar-options.ts +53 -0
- package/src/native/shared/vertical-bars-geometry.ts +80 -0
- package/src/native/startup.ts +84 -0
- package/src/native/vertical-bars-web-search.ts +86 -0
- package/src/native/vertical-bars-web.ts +498 -0
- package/src/native-integration/index.ts +49 -0
- package/src/vertical-bars.ts +11 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { fitsClip } from './shared/clipping';
|
|
2
|
+
import type { NativeUIRuntimeOptions, NativeUIRuntimeHandle } from './definitions';
|
|
3
|
+
import { appendItem, createCandidate, type Candidate, type Identify } from './shared/candidate';
|
|
4
|
+
import { marker, unprojected, visible } from './shared/dom';
|
|
5
|
+
|
|
6
|
+
/** Standard HTML sources use standard native renderers, not app-local Swift extensions. */
|
|
7
|
+
export const htmlSelector = '[data-native-ui="button"], [data-native-ui="back-button"]';
|
|
8
|
+
export const htmlRailSource = (element: Element) =>
|
|
9
|
+
element.matches(htmlSelector) && element.getAttribute('data-native-ui-placement') === 'rail';
|
|
10
|
+
|
|
11
|
+
export const readHTMLControl = (element: HTMLElement, id: Identify, options: NativeUIRuntimeOptions): Candidate | undefined => {
|
|
12
|
+
if (
|
|
13
|
+
!(options.root ?? element.ownerDocument.body).contains(element) ||
|
|
14
|
+
!element.matches(htmlSelector) ||
|
|
15
|
+
element.closest('[data-native-ui-proxy]')
|
|
16
|
+
)
|
|
17
|
+
return;
|
|
18
|
+
// Explicit opt-in; custom elements can implement click() and expose role=button.
|
|
19
|
+
if (!element.matches('button, ion-button, [role="button"]') || !visible(element, htmlRailSource(element))) return;
|
|
20
|
+
if (element.contains(element.ownerDocument.activeElement)) return;
|
|
21
|
+
if (!htmlRailSource(element) && !fitsClip(element)) return;
|
|
22
|
+
if (
|
|
23
|
+
options.explicitControls === false ||
|
|
24
|
+
(options.explicitControls === undefined && (options.controls?.toolbar === false || options.controls?.buttons === false))
|
|
25
|
+
)
|
|
26
|
+
return;
|
|
27
|
+
if (htmlRailSource(element) && options.verticalBars?.enabled === false) return;
|
|
28
|
+
const kind = element.getAttribute('data-native-ui') === 'back-button' ? 'back-button' : 'button';
|
|
29
|
+
const candidate = createCandidate(element, kind, id, options);
|
|
30
|
+
const item = appendItem(candidate, element, id);
|
|
31
|
+
if (!item) return;
|
|
32
|
+
const labelledBy = element
|
|
33
|
+
.getAttribute('aria-labelledby')
|
|
34
|
+
?.trim()
|
|
35
|
+
.split(/\s+/)
|
|
36
|
+
.map((key) => element.ownerDocument.getElementById(key)?.textContent?.trim() ?? '')
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
.join(' ');
|
|
39
|
+
item.accessibilityLabel = labelledBy || element.getAttribute('aria-label') || item.label;
|
|
40
|
+
if (!item.accessibilityLabel) return;
|
|
41
|
+
item.disabled ||= element.matches(':disabled, [disabled]') || !!element.closest('[aria-disabled="true"]');
|
|
42
|
+
if (htmlRailSource(element)) candidate.control.placement = 'vertical-bars';
|
|
43
|
+
return candidate;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** A rail host is supplied and styled by the application, never by the plugin. */
|
|
47
|
+
export const createHTMLWebProjection = (doc: Document, options: NativeUIRuntimeOptions): NativeUIRuntimeHandle => {
|
|
48
|
+
const host = options.rail?.host;
|
|
49
|
+
if (!host) {
|
|
50
|
+
let stopped = false;
|
|
51
|
+
return {
|
|
52
|
+
markForCheck: () => {},
|
|
53
|
+
getStatus: () => ({ state: stopped ? 'stopped' : 'web', projected: 0, updates: 0 }),
|
|
54
|
+
suspend: async () => ({ resume: async () => {} }),
|
|
55
|
+
destroy: async () => {
|
|
56
|
+
stopped = true;
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const win = doc.defaultView!;
|
|
61
|
+
const owned = new Map<HTMLElement, { clone: HTMLButtonElement; aria: string | null; signature: string }>();
|
|
62
|
+
const style = doc.createElement('style');
|
|
63
|
+
style.textContent = `[data-native-ui-web-source] { visibility: hidden !important; pointer-events: none !important; }`;
|
|
64
|
+
doc.head.append(style);
|
|
65
|
+
let frame = 0;
|
|
66
|
+
let stopped = false;
|
|
67
|
+
let suspended = 0;
|
|
68
|
+
let updates = 0;
|
|
69
|
+
const restore = (source: HTMLElement) => {
|
|
70
|
+
const state = owned.get(source);
|
|
71
|
+
if (!state) return;
|
|
72
|
+
const focused = state.clone.contains(doc.activeElement);
|
|
73
|
+
state.clone.remove();
|
|
74
|
+
source.removeAttribute('data-native-ui-web-source');
|
|
75
|
+
if (focused && source.isConnected && !source.hasAttribute(marker)) source.focus();
|
|
76
|
+
owned.delete(source);
|
|
77
|
+
};
|
|
78
|
+
const measure = <T>(source: HTMLElement, read: () => T): T => {
|
|
79
|
+
const hidden = source.hasAttribute('data-native-ui-web-source');
|
|
80
|
+
if (hidden) source.removeAttribute('data-native-ui-web-source');
|
|
81
|
+
try {
|
|
82
|
+
return unprojected([source], read);
|
|
83
|
+
} finally {
|
|
84
|
+
if (hidden) source.setAttribute('data-native-ui-web-source', '');
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const render = () => {
|
|
88
|
+
frame = 0;
|
|
89
|
+
if (stopped) return;
|
|
90
|
+
observer.disconnect();
|
|
91
|
+
observer.observe(doc.documentElement, { subtree: true, childList: true, attributes: true, characterData: true });
|
|
92
|
+
const wanted = new Set<HTMLElement>();
|
|
93
|
+
if (!suspended && options.verticalBars?.enabled !== false && host?.isConnected && options.root?.isConnected) {
|
|
94
|
+
const sources = Array.from(options.root.querySelectorAll<HTMLElement>(htmlSelector));
|
|
95
|
+
let back = false;
|
|
96
|
+
for (const source of sources) {
|
|
97
|
+
// An accepted native surface owns its source and descendants. Rejected sources
|
|
98
|
+
// lose the marker and can return to the Web rail on the next render.
|
|
99
|
+
if (!htmlRailSource(source) || host.contains(source) || source.closest(`[${marker}]`)) continue;
|
|
100
|
+
const iconRoot = source.querySelector('ion-icon')?.shadowRoot;
|
|
101
|
+
if (iconRoot) observer.observe(iconRoot, { subtree: true, childList: true, attributes: true, characterData: true });
|
|
102
|
+
const candidate = measure(source, () => readHTMLControl(source, () => 'web', options));
|
|
103
|
+
if (!candidate) continue;
|
|
104
|
+
if (candidate.control.kind === 'back-button') {
|
|
105
|
+
if (back) continue;
|
|
106
|
+
back = true;
|
|
107
|
+
}
|
|
108
|
+
wanted.add(source);
|
|
109
|
+
const item = candidate.control.items[0];
|
|
110
|
+
const svg = source.querySelector('svg') ?? iconRoot?.querySelector('svg');
|
|
111
|
+
const signature = JSON.stringify([item, source.innerHTML, svg?.outerHTML]);
|
|
112
|
+
let state = owned.get(source);
|
|
113
|
+
if (!state) {
|
|
114
|
+
const clone = doc.createElement('button');
|
|
115
|
+
clone.type = 'button';
|
|
116
|
+
clone.setAttribute('data-native-ui-proxy', candidate.control.kind);
|
|
117
|
+
state = { clone, aria: source.getAttribute('aria-hidden'), signature: '' };
|
|
118
|
+
owned.set(source, state);
|
|
119
|
+
clone.addEventListener('click', () => {
|
|
120
|
+
const current = !stopped && !suspended && measure(source, () => readHTMLControl(source, () => 'web', options));
|
|
121
|
+
if (
|
|
122
|
+
options.verticalBars?.enabled === false ||
|
|
123
|
+
!current ||
|
|
124
|
+
!htmlRailSource(source) ||
|
|
125
|
+
!host.isConnected ||
|
|
126
|
+
current.control.items[0].disabled ||
|
|
127
|
+
source.closest(`[${marker}]`)
|
|
128
|
+
)
|
|
129
|
+
return;
|
|
130
|
+
source.click(); // Source owns submission, custom handlers and back navigation.
|
|
131
|
+
schedule();
|
|
132
|
+
});
|
|
133
|
+
host.append(clone);
|
|
134
|
+
}
|
|
135
|
+
if (state.signature !== signature) {
|
|
136
|
+
state.signature = signature;
|
|
137
|
+
state.clone.replaceChildren();
|
|
138
|
+
if (svg) {
|
|
139
|
+
const icon = svg.cloneNode(true) as SVGElement;
|
|
140
|
+
icon.querySelectorAll('[id]').forEach((node) => node.removeAttribute('id'));
|
|
141
|
+
icon.removeAttribute('id');
|
|
142
|
+
for (const node of [icon, ...Array.from(icon.querySelectorAll('*'))])
|
|
143
|
+
for (const attr of Array.from(node.attributes)) if (attr.name.startsWith('on')) node.removeAttribute(attr.name);
|
|
144
|
+
state.clone.append(icon);
|
|
145
|
+
}
|
|
146
|
+
state.clone.append(doc.createTextNode(item.label));
|
|
147
|
+
state.clone.setAttribute('aria-label', item.accessibilityLabel);
|
|
148
|
+
state.clone.disabled = item.disabled;
|
|
149
|
+
updates++;
|
|
150
|
+
}
|
|
151
|
+
if (!source.hasAttribute('data-native-ui-web-source')) source.setAttribute('data-native-ui-web-source', '');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
for (const source of owned.keys()) if (!wanted.has(source)) restore(source);
|
|
155
|
+
// Ignore our own synchronous DOM writes; later application mutations still schedule a refresh.
|
|
156
|
+
observer.takeRecords();
|
|
157
|
+
};
|
|
158
|
+
const schedule = () => {
|
|
159
|
+
if (!stopped && !frame) frame = win.requestAnimationFrame(render);
|
|
160
|
+
};
|
|
161
|
+
const observer = new MutationObserver(schedule);
|
|
162
|
+
win.addEventListener('resize', schedule);
|
|
163
|
+
win.addEventListener('nativeUIRefresh', schedule);
|
|
164
|
+
render();
|
|
165
|
+
return {
|
|
166
|
+
markForCheck: schedule,
|
|
167
|
+
getStatus: () => ({ state: stopped ? 'stopped' : 'web', projected: owned.size, updates }),
|
|
168
|
+
async suspend() {
|
|
169
|
+
suspended++;
|
|
170
|
+
for (const source of owned.keys()) restore(source);
|
|
171
|
+
let resumed = false;
|
|
172
|
+
return {
|
|
173
|
+
async resume() {
|
|
174
|
+
if (!resumed) {
|
|
175
|
+
resumed = true;
|
|
176
|
+
suspended--;
|
|
177
|
+
render();
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
},
|
|
182
|
+
async destroy() {
|
|
183
|
+
if (stopped) return;
|
|
184
|
+
stopped = true;
|
|
185
|
+
observer.disconnect();
|
|
186
|
+
win.cancelAnimationFrame(frame);
|
|
187
|
+
win.removeEventListener('resize', schedule);
|
|
188
|
+
win.removeEventListener('nativeUIRefresh', schedule);
|
|
189
|
+
for (const source of owned.keys()) restore(source);
|
|
190
|
+
style.remove();
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Compatibility Ionic entrypoint. Standard HTML consumers can import /core without Ionic.
|
|
2
|
+
import { startNativeUI as activate } from './startup';
|
|
3
|
+
import type { NativeUIOptions, VerticalControlAreaOptions, VerticalControlAreaHandle } from './definitions';
|
|
4
|
+
import { setVerticalControlAreaPlacement } from './legacy-layout';
|
|
5
|
+
export { setVerticalControlAreaPlacement } from './legacy-layout';
|
|
6
|
+
export { configureNativeTransition, NativeUI } from './activation';
|
|
7
|
+
export { defineNativeUI } from './extensions';
|
|
8
|
+
export type { NativeUIExtension, NativeUIValue, NativeUIReadContext, NativeUIImageReference } from './extensions';
|
|
9
|
+
export type {
|
|
10
|
+
NativeUIVerticalBarsOptions,
|
|
11
|
+
NativeUIComponent,
|
|
12
|
+
NativeUIControls,
|
|
13
|
+
AutoIonicControls,
|
|
14
|
+
IonicProjectionOptions,
|
|
15
|
+
NativeUIHandle,
|
|
16
|
+
NativeUIOptions,
|
|
17
|
+
NativeUIStatus,
|
|
18
|
+
NativeUISuspension,
|
|
19
|
+
VerticalBarEdge,
|
|
20
|
+
VerticalBarPlacement,
|
|
21
|
+
VerticalControlAreaHandle,
|
|
22
|
+
ToolbarProjectionOptions,
|
|
23
|
+
VerticalControlAreaOptions,
|
|
24
|
+
WebViewMetrics,
|
|
25
|
+
} from './definitions';
|
|
26
|
+
|
|
27
|
+
export const enableNativeUI = (options: NativeUIOptions = {}) =>
|
|
28
|
+
activate(options, async (...args) => (await import('./overlays/controller')).createOverlayController(...args));
|
|
29
|
+
|
|
30
|
+
export const enableVerticalControlArea = async (options: VerticalControlAreaOptions = {}): Promise<VerticalControlAreaHandle> => {
|
|
31
|
+
const handle = await enableNativeUI({
|
|
32
|
+
extensions: options.extensions,
|
|
33
|
+
horizontal: options.horizontal,
|
|
34
|
+
vertical: options.vertical,
|
|
35
|
+
buttonProjection: options.buttonProjection,
|
|
36
|
+
buttonDefaultFill: options.buttonDefaultFill,
|
|
37
|
+
controls: { tabs: true, toolbar: true, popover: options.controls?.popover === true },
|
|
38
|
+
verticalBarsOnly: true,
|
|
39
|
+
});
|
|
40
|
+
return { ...handle, setPlacement: setVerticalControlAreaPlacement };
|
|
41
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { VerticalBarEdge, VerticalBarPlacement } from './definitions';
|
|
2
|
+
const physicalVerticalBarEdge = (edge: Exclude<VerticalBarEdge, null>, rtl: boolean) => ((edge === 'leading') !== rtl ? 'left' : 'right');
|
|
3
|
+
const elementRtl = (element: Element): boolean => element.closest('[dir]')?.getAttribute('dir') === 'rtl';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @deprecated Use the theme setVerticalBarsLayout helper. nativeEdge is ignored; device metrics determine availability.
|
|
7
|
+
* Legacy Web layout adapter; does not report native availability.
|
|
8
|
+
* Pass `rtl` when the document direction is known; otherwise the nearest `dir` attribute is used.
|
|
9
|
+
*/
|
|
10
|
+
export const setVerticalControlAreaPlacement = (placement: VerticalBarEdge | VerticalBarPlacement, rtl?: boolean): void => {
|
|
11
|
+
if (typeof document === 'undefined') return;
|
|
12
|
+
const app = document.querySelector<HTMLElement>('ion-app');
|
|
13
|
+
if (!app) throw new Error('Vertical Control Area requires ion-app');
|
|
14
|
+
const { edge, inset = 0 } = placement && typeof placement === 'object' ? placement : { edge: placement, inset: 0 };
|
|
15
|
+
app.removeAttribute('data-native-ui-vertical-bars-suspended');
|
|
16
|
+
app.classList.toggle('ios-theme-vertical-bars', edge !== null);
|
|
17
|
+
app.classList.toggle('ios-theme-vertical-bars-left', edge !== null && physicalVerticalBarEdge(edge, rtl ?? elementRtl(app)) === 'left');
|
|
18
|
+
if (edge && Number.isFinite(inset) && inset > 0) app.style.setProperty('--ios-theme-vertical-bars-native-inset', `${inset}px`);
|
|
19
|
+
else app.style.removeProperty('--ios-theme-vertical-bars-native-inset');
|
|
20
|
+
document.defaultView?.dispatchEvent(new Event('nativeUIRefresh'));
|
|
21
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { sheetText } from './styles';
|
|
2
|
+
|
|
3
|
+
/** Constructed stylesheets cannot cross documents with their nodes. */
|
|
4
|
+
export const moveContent = (content: HTMLElement[], destination: HTMLElement): (() => void) => {
|
|
5
|
+
const home = content.map((element) => ({ element, parent: element.parentNode!, next: element.nextSibling }));
|
|
6
|
+
const roots: { root: ShadowRoot; css: string; sheets: CSSStyleSheet[] }[] = [];
|
|
7
|
+
const collect = (node: Element) => {
|
|
8
|
+
if (node.shadowRoot) {
|
|
9
|
+
roots.push({
|
|
10
|
+
root: node.shadowRoot,
|
|
11
|
+
css: sheetText(node.shadowRoot.adoptedStyleSheets),
|
|
12
|
+
sheets: [...node.shadowRoot.adoptedStyleSheets],
|
|
13
|
+
});
|
|
14
|
+
for (const child of Array.from(node.shadowRoot.children)) collect(child);
|
|
15
|
+
}
|
|
16
|
+
for (const child of Array.from(node.children)) collect(child);
|
|
17
|
+
};
|
|
18
|
+
content.forEach(collect);
|
|
19
|
+
for (const element of content) destination.append(destination.ownerDocument.adoptNode(element));
|
|
20
|
+
const styles = roots
|
|
21
|
+
.filter(({ css }) => css)
|
|
22
|
+
.map(({ root, css }) => {
|
|
23
|
+
const style = destination.ownerDocument.createElement('style');
|
|
24
|
+
style.textContent = css;
|
|
25
|
+
root.append(style);
|
|
26
|
+
return style;
|
|
27
|
+
});
|
|
28
|
+
return () => {
|
|
29
|
+
styles.forEach((style) => style.remove());
|
|
30
|
+
for (const { element, parent, next } of home) {
|
|
31
|
+
const connected = element.isConnected;
|
|
32
|
+
parent.ownerDocument!.adoptNode(element);
|
|
33
|
+
// Ionic may already have destroyed the Angular component on dismiss.
|
|
34
|
+
if (connected) parent.insertBefore(element, next?.parentNode === parent ? next : null);
|
|
35
|
+
}
|
|
36
|
+
for (const { root, css, sheets } of roots) {
|
|
37
|
+
// WebKit re-associates adopted sheets with the new document across
|
|
38
|
+
// adoptNode, so they may no longer match this root; fall back to the
|
|
39
|
+
// inline copy instead of aborting the remaining restore.
|
|
40
|
+
try {
|
|
41
|
+
root.adoptedStyleSheets = sheets;
|
|
42
|
+
} catch {
|
|
43
|
+
if (css) {
|
|
44
|
+
const style = root.ownerDocument.createElement('style');
|
|
45
|
+
style.textContent = css;
|
|
46
|
+
root.append(style);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
};
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import type { NativeUIOptions, NativeUIPlugin } from '../definitions';
|
|
2
|
+
import { bounded, boundedListener } from '../shared/bridge';
|
|
3
|
+
import { isExcluded, isPermanentlyExcluded, isNativeUIDisabled } from '../shared/dom';
|
|
4
|
+
import { activeElement, trapFocus } from './focus';
|
|
5
|
+
import { relayPopover, popoverPresentation, verticalBarsPopoverAnchor } from './popover';
|
|
6
|
+
import { nativeDialogAnimation } from './dialog-animation';
|
|
7
|
+
import { relayLayoutSupported, relayStyles } from './styles';
|
|
8
|
+
import { moveContent } from './content';
|
|
9
|
+
import { validVerticalBarFrame } from '../shared/vertical-bars-geometry';
|
|
10
|
+
|
|
11
|
+
type Presentation = { kind: 'popover'; animated: boolean };
|
|
12
|
+
type Overlay = HTMLIonPopoverElement;
|
|
13
|
+
const lifecycle = (overlay: Overlay, phase: string) =>
|
|
14
|
+
`ion${overlay.localName.slice(4, 5).toUpperCase()}${overlay.localName.slice(5)}${phase}`;
|
|
15
|
+
|
|
16
|
+
interface Connection {
|
|
17
|
+
id: string;
|
|
18
|
+
overlay: Overlay;
|
|
19
|
+
dismissed: boolean;
|
|
20
|
+
win?: Window;
|
|
21
|
+
focus: Element | null;
|
|
22
|
+
stopStyles?: () => void;
|
|
23
|
+
releaseProjection?: () => void;
|
|
24
|
+
stopContent?: () => void;
|
|
25
|
+
stopFocus?: () => void;
|
|
26
|
+
stopEvents?: () => void;
|
|
27
|
+
stopChildEvents?: () => void;
|
|
28
|
+
stopExclusions?: () => void;
|
|
29
|
+
closing?: Promise<void>;
|
|
30
|
+
nativeClosing?: Promise<void>;
|
|
31
|
+
stopAnimation?: () => void;
|
|
32
|
+
hideAnimation?: () => void;
|
|
33
|
+
stopLifecycle?: () => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Ionic owns lifecycle and dismissal; only one overlay is relayed at a time. */
|
|
37
|
+
export const createOverlayController = async (
|
|
38
|
+
doc: Document,
|
|
39
|
+
plugin: NativeUIPlugin,
|
|
40
|
+
options: NativeUIOptions,
|
|
41
|
+
retainProjection: (deferActivation?: (id: string) => boolean) => () => void,
|
|
42
|
+
nativeVerticalBars: () => boolean,
|
|
43
|
+
) => {
|
|
44
|
+
let current: Connection | undefined;
|
|
45
|
+
let opening = Promise.resolve();
|
|
46
|
+
let sequence = 0;
|
|
47
|
+
let stopped = false;
|
|
48
|
+
const suspensions = new Set<symbol>();
|
|
49
|
+
const events = ['Modal', 'Popover', 'Alert', 'ActionSheet', 'Loading', 'Picker', 'Toast'].map((name) => `ion${name}WillPresent`);
|
|
50
|
+
const cleanup = async (connection: Connection) => {
|
|
51
|
+
// Every teardown step is isolated and bounded: a throwing or hanging step
|
|
52
|
+
// must not strand the retained page projections or the child window.
|
|
53
|
+
const stage = async (label: string, run?: () => void | Promise<void>) => {
|
|
54
|
+
try {
|
|
55
|
+
await bounded(Promise.resolve(run?.()));
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error(`Native overlay teardown failed at ${label}; continuing cleanup anyway.`, error);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
try {
|
|
61
|
+
await stage('events', connection.stopEvents);
|
|
62
|
+
await stage('lifecycle', connection.stopLifecycle);
|
|
63
|
+
await stage('childEvents', connection.stopChildEvents);
|
|
64
|
+
await stage('exclusions', connection.stopExclusions);
|
|
65
|
+
await stage('focus', connection.stopFocus);
|
|
66
|
+
// A timed-out dismissal must not strand the relayed content in the child window.
|
|
67
|
+
await connection.nativeClosing?.catch((error) => {
|
|
68
|
+
console.error('Native overlay dismissal did not complete; restoring the Web content anyway.', error);
|
|
69
|
+
});
|
|
70
|
+
await stage('content', connection.stopContent);
|
|
71
|
+
await stage('animation', connection.stopAnimation);
|
|
72
|
+
await stage('styles', connection.stopStyles);
|
|
73
|
+
await stage('close', () => bounded(plugin.closeOverlay({ id: connection.id })));
|
|
74
|
+
} finally {
|
|
75
|
+
// The page projections must be released even if teardown throws above.
|
|
76
|
+
connection.releaseProjection?.();
|
|
77
|
+
connection.releaseProjection = undefined;
|
|
78
|
+
try {
|
|
79
|
+
connection.win?.close();
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error('Native overlay window was already gone.', error);
|
|
82
|
+
}
|
|
83
|
+
if (current === connection) current = undefined;
|
|
84
|
+
const focus = connection.focus as HTMLElement | null;
|
|
85
|
+
if (focus?.isConnected && focus.tabIndex >= 0) focus.focus({ preventScroll: true });
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
const release = (connection: Connection): Promise<void> => (connection.closing ??= cleanup(connection));
|
|
89
|
+
const restoreWeb = async () => {
|
|
90
|
+
if (current) await release(current);
|
|
91
|
+
};
|
|
92
|
+
const excluded = (overlay: HTMLElement) => isPermanentlyExcluded(overlay) || isExcluded(overlay) || isNativeUIDisabled(overlay);
|
|
93
|
+
const connect = async (connection: Connection, ready: Promise<void>, presentation: Presentation) => {
|
|
94
|
+
const { overlay } = connection;
|
|
95
|
+
const gone = () =>
|
|
96
|
+
current !== connection ||
|
|
97
|
+
stopped ||
|
|
98
|
+
suspensions.size ||
|
|
99
|
+
connection.dismissed ||
|
|
100
|
+
!overlay.isConnected ||
|
|
101
|
+
!nativeVerticalBars() ||
|
|
102
|
+
excluded(overlay);
|
|
103
|
+
// Keep the source invisible while the native toolbar prepares its popover.
|
|
104
|
+
connection.hideAnimation?.();
|
|
105
|
+
try {
|
|
106
|
+
// Prepare the child window concurrently with Ionic's content initialization.
|
|
107
|
+
const staging = (async () => {
|
|
108
|
+
await bounded(
|
|
109
|
+
plugin.prepareOverlay({
|
|
110
|
+
id: connection.id,
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
if (gone()) return undefined;
|
|
114
|
+
const win = doc.defaultView!.open(`about:blank#${connection.id}`, '_blank');
|
|
115
|
+
if (!win) throw new Error('Native overlay window unavailable');
|
|
116
|
+
connection.win = win;
|
|
117
|
+
// The requested blank page replaces the initial document created by window.open.
|
|
118
|
+
// WebKit percent-encodes the '#' marker in the popup URL; compare decoded.
|
|
119
|
+
// Adopt nodes only after that navigation; otherwise WebKit discards the relay.
|
|
120
|
+
if (decodeURIComponent(win.document.URL) !== `about:blank#${connection.id}` || win.document.readyState !== 'complete')
|
|
121
|
+
await bounded(new Promise<void>((resolve) => win.addEventListener('load', () => resolve(), { once: true })));
|
|
122
|
+
if (gone()) return undefined;
|
|
123
|
+
// about:blank is a quirks-mode document in WebKit. Initialize standards
|
|
124
|
+
// mode before adopting live content: otherwise percentage button heights
|
|
125
|
+
// can resolve against the entire popover viewport.
|
|
126
|
+
win.document.open();
|
|
127
|
+
win.document.write('<!doctype html><html><head></head><body></body></html>');
|
|
128
|
+
win.document.close();
|
|
129
|
+
connection.stopChildEvents = observe(win.document);
|
|
130
|
+
const environment = relayStyles(doc, win.document, overlay, () => {
|
|
131
|
+
if (connection.stopContent) void release(connection).catch(console.error);
|
|
132
|
+
});
|
|
133
|
+
connection.stopStyles = environment.stop;
|
|
134
|
+
await bounded(environment.ready);
|
|
135
|
+
if (gone()) return undefined;
|
|
136
|
+
return { destination: environment.destination, win };
|
|
137
|
+
})();
|
|
138
|
+
// The staging join is awaited below; settle early rejections quietly.
|
|
139
|
+
staging.catch(() => {});
|
|
140
|
+
// Ionic initializes its gestures after didPresent. Let that turn finish before adopting their content.
|
|
141
|
+
// A stalled presentation must not park the opening queue forever.
|
|
142
|
+
await bounded(ready);
|
|
143
|
+
// Placement is captured after Ionic has laid out the overlay.
|
|
144
|
+
await bounded(new Promise<void>((resolve) => doc.defaultView!.requestAnimationFrame(() => resolve())));
|
|
145
|
+
const staged = await staging;
|
|
146
|
+
if (gone() || !staged) {
|
|
147
|
+
await release(connection);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const focused = activeElement(doc) as HTMLElement | null;
|
|
151
|
+
const nativePresentation = popoverPresentation(overlay);
|
|
152
|
+
const content = relayPopover(overlay, staged.destination);
|
|
153
|
+
connection.stopContent = content.stop;
|
|
154
|
+
if (!relayLayoutSupported(overlay) || !content.matchesSourceAppearance()) {
|
|
155
|
+
await release(connection);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
connection.hideAnimation?.();
|
|
159
|
+
// Ionic does not dismiss a presented overlay when its page is hidden or
|
|
160
|
+
// destroyed; without this a popover could retain the page projections
|
|
161
|
+
// forever and freeze every later control update.
|
|
162
|
+
const exclusions = new MutationObserver(() => {
|
|
163
|
+
if (!overlay.isConnected || !nativeVerticalBars() || excluded(overlay) || isNativeUIDisabled(content.root))
|
|
164
|
+
void release(connection).catch(console.error);
|
|
165
|
+
});
|
|
166
|
+
const attributes = { attributes: true, attributeFilter: ['class', 'data-native-ui', 'hidden', 'inert'] };
|
|
167
|
+
for (let node: HTMLElement | null = overlay; node; node = node.parentElement) {
|
|
168
|
+
exclusions.observe(node, attributes);
|
|
169
|
+
exclusions.observe(node, { childList: true });
|
|
170
|
+
}
|
|
171
|
+
exclusions.observe(content.root, { ...attributes, subtree: true });
|
|
172
|
+
connection.stopExclusions = () => exclusions.disconnect();
|
|
173
|
+
connection.stopFocus = trapFocus(
|
|
174
|
+
content.root,
|
|
175
|
+
() =>
|
|
176
|
+
!connection.dismissed &&
|
|
177
|
+
(!('focusTrap' in overlay) || overlay.focusTrap !== false) &&
|
|
178
|
+
!overlay.classList.contains('ion-disable-focus-trap'),
|
|
179
|
+
);
|
|
180
|
+
const close = () => {
|
|
181
|
+
void release(connection).catch(console.error);
|
|
182
|
+
};
|
|
183
|
+
const escape = (event: KeyboardEvent) => {
|
|
184
|
+
if (event.key === 'Escape' && overlay.backdropDismiss) void overlay.dismiss(undefined, 'backdrop');
|
|
185
|
+
};
|
|
186
|
+
staged.win.document.addEventListener('keydown', escape);
|
|
187
|
+
overlay.addEventListener(lifecycle(overlay, 'DidDismiss'), close, { once: true });
|
|
188
|
+
connection.stopEvents = () => {
|
|
189
|
+
staged.win.document.removeEventListener('keydown', escape);
|
|
190
|
+
overlay.removeEventListener(lifecycle(overlay, 'DidDismiss'), close);
|
|
191
|
+
};
|
|
192
|
+
// SwiftUI owns the only visible presentation animation.
|
|
193
|
+
await bounded(
|
|
194
|
+
plugin.presentOverlay({
|
|
195
|
+
id: connection.id,
|
|
196
|
+
presentation: { ...nativePresentation, animated: presentation.animated },
|
|
197
|
+
}),
|
|
198
|
+
);
|
|
199
|
+
(focused?.isConnected && focused.ownerDocument === staged.win.document ? focused : content.root).focus({ preventScroll: true });
|
|
200
|
+
} catch (error) {
|
|
201
|
+
await release(connection);
|
|
202
|
+
console.error('Native overlay projection failed; keeping Ionic content in the source WebView.', error);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
const present = (event: Event) => {
|
|
206
|
+
const overlay = event.target as Overlay;
|
|
207
|
+
if (current) {
|
|
208
|
+
const connection = current;
|
|
209
|
+
const fromChild = overlay.ownerDocument === connection.win?.document;
|
|
210
|
+
if (fromChild) {
|
|
211
|
+
// A child-document overlay may be a sibling of the relayed content.
|
|
212
|
+
// Move it before cleanup closes that document; keep the live node,
|
|
213
|
+
// component handlers and adopted shadow styles in the source WebView.
|
|
214
|
+
moveContent([overlay], doc.querySelector<HTMLElement>('ion-app') ?? doc.body);
|
|
215
|
+
}
|
|
216
|
+
current = undefined;
|
|
217
|
+
opening = opening.then(() => release(connection)).catch(console.error);
|
|
218
|
+
// The original event path still belongs to the child document. Let the
|
|
219
|
+
// source runtime observe presentation before releasing retained controls.
|
|
220
|
+
if (fromChild) doc.dispatchEvent(new CustomEvent('nativeUIOverlayAdopted', { detail: overlay }));
|
|
221
|
+
if (!connection.dismissed) return;
|
|
222
|
+
// A didDismiss handler may immediately open the next popover while SwiftUI is still closing this one.
|
|
223
|
+
connection.stopAnimation?.();
|
|
224
|
+
connection.stopAnimation = undefined;
|
|
225
|
+
}
|
|
226
|
+
const enabled =
|
|
227
|
+
overlay.localName === 'ion-popover' &&
|
|
228
|
+
overlay.mode !== 'md' &&
|
|
229
|
+
!overlay.classList.contains('md') &&
|
|
230
|
+
options.controls?.popover === true &&
|
|
231
|
+
nativeVerticalBars() &&
|
|
232
|
+
verticalBarsPopoverAnchor(overlay) !== undefined;
|
|
233
|
+
if (stopped || suspensions.size || !enabled || excluded(overlay)) {
|
|
234
|
+
opening = opening.then(restoreWeb).catch(console.error);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const connection: Connection = { id: `ios-theme-overlay-${++sequence}`, overlay, dismissed: false, focus: activeElement(doc) };
|
|
238
|
+
const presentation: Presentation = { kind: 'popover', animated: overlay.animated };
|
|
239
|
+
current = connection;
|
|
240
|
+
// Retain synchronously, before the scheduled projection update can retire the rail.
|
|
241
|
+
const anchorId = verticalBarsPopoverAnchor(overlay);
|
|
242
|
+
connection.releaseProjection = retainProjection(
|
|
243
|
+
(id) =>
|
|
244
|
+
id === anchorId &&
|
|
245
|
+
current === connection &&
|
|
246
|
+
!!connection.nativeClosing &&
|
|
247
|
+
!stopped &&
|
|
248
|
+
!suspensions.size &&
|
|
249
|
+
nativeVerticalBars() &&
|
|
250
|
+
!excluded(overlay),
|
|
251
|
+
);
|
|
252
|
+
const closed = () => {
|
|
253
|
+
connection.dismissed = true;
|
|
254
|
+
};
|
|
255
|
+
overlay.addEventListener(lifecycle(overlay, 'DidDismiss'), closed, { once: true });
|
|
256
|
+
let resolveReady!: () => void;
|
|
257
|
+
const readyPromise = new Promise<void>((resolve) => {
|
|
258
|
+
resolveReady = resolve;
|
|
259
|
+
});
|
|
260
|
+
const ready = () => {
|
|
261
|
+
resolveReady();
|
|
262
|
+
};
|
|
263
|
+
const closeNative = () =>
|
|
264
|
+
(connection.nativeClosing ??= bounded(plugin.dismissOverlay({ id: connection.id, animated: presentation.animated })));
|
|
265
|
+
const viewport = `${win.innerWidth}:${win.innerHeight}`;
|
|
266
|
+
const anchor = bounded(plugin.getVerticalBarsGeometry({ viewportWidth: win.innerWidth, ids: [anchorId!] }))
|
|
267
|
+
.then(({ items }) => {
|
|
268
|
+
const frame = items[anchorId!];
|
|
269
|
+
return frame && validVerticalBarFrame(frame) && viewport === `${win.innerWidth}:${win.innerHeight}` ? frame : undefined;
|
|
270
|
+
})
|
|
271
|
+
.catch(() => undefined);
|
|
272
|
+
const animation = nativeDialogAnimation(overlay, closeNative, anchor);
|
|
273
|
+
connection.stopAnimation = animation.stop;
|
|
274
|
+
connection.hideAnimation = animation.hide;
|
|
275
|
+
animation.hide();
|
|
276
|
+
overlay.addEventListener(lifecycle(overlay, 'DidPresent'), ready, { once: true });
|
|
277
|
+
connection.stopLifecycle = () => overlay.removeEventListener(lifecycle(overlay, 'DidDismiss'), closed);
|
|
278
|
+
opening = opening
|
|
279
|
+
.then(() => connect(connection, readyPromise, presentation))
|
|
280
|
+
.catch(console.error)
|
|
281
|
+
.finally(() => {
|
|
282
|
+
overlay.removeEventListener(lifecycle(overlay, 'DidPresent'), ready);
|
|
283
|
+
});
|
|
284
|
+
};
|
|
285
|
+
const menuOpen = (event: Event) => {
|
|
286
|
+
if (!event.composedPath().some((node): node is HTMLElement => node instanceof HTMLElement && node.matches('ion-menu'))) return;
|
|
287
|
+
// Menus render in the source document below the native overlay; release the relay.
|
|
288
|
+
const connection = current;
|
|
289
|
+
current = undefined;
|
|
290
|
+
if (connection) opening = opening.then(() => release(connection)).catch(console.error);
|
|
291
|
+
};
|
|
292
|
+
const observe = (target: Document) => {
|
|
293
|
+
for (const name of events) target.addEventListener(name, present);
|
|
294
|
+
target.addEventListener('ionWillOpen', menuOpen);
|
|
295
|
+
return () => {
|
|
296
|
+
for (const name of events) target.removeEventListener(name, present);
|
|
297
|
+
target.removeEventListener('ionWillOpen', menuOpen);
|
|
298
|
+
};
|
|
299
|
+
};
|
|
300
|
+
const invalidate = () => {
|
|
301
|
+
const connection = current;
|
|
302
|
+
current = undefined;
|
|
303
|
+
if (connection) opening = opening.then(() => release(connection)).catch(console.error);
|
|
304
|
+
};
|
|
305
|
+
// Retention must not freeze an obsolete layout or a departed page.
|
|
306
|
+
const win = doc.defaultView!;
|
|
307
|
+
const visibility = () => {
|
|
308
|
+
if (doc.hidden) invalidate();
|
|
309
|
+
};
|
|
310
|
+
const listener = await boundedListener(
|
|
311
|
+
plugin.addListener('overlay', (event) => {
|
|
312
|
+
if (event.id !== current?.id) return;
|
|
313
|
+
if (event.action === 'dismiss' && current.overlay.backdropDismiss)
|
|
314
|
+
void current.overlay.dismiss(undefined, 'backdrop').catch(console.error);
|
|
315
|
+
}),
|
|
316
|
+
);
|
|
317
|
+
win.addEventListener('resize', invalidate);
|
|
318
|
+
doc.addEventListener('ionViewWillLeave', invalidate);
|
|
319
|
+
doc.addEventListener('visibilitychange', visibility);
|
|
320
|
+
const stopEvents = observe(doc);
|
|
321
|
+
return {
|
|
322
|
+
async suspend() {
|
|
323
|
+
const lease = Symbol();
|
|
324
|
+
suspensions.add(lease);
|
|
325
|
+
await opening;
|
|
326
|
+
await restoreWeb();
|
|
327
|
+
return () => {
|
|
328
|
+
suspensions.delete(lease);
|
|
329
|
+
};
|
|
330
|
+
},
|
|
331
|
+
async destroy() {
|
|
332
|
+
stopped = true;
|
|
333
|
+
stopEvents();
|
|
334
|
+
win.removeEventListener('resize', invalidate);
|
|
335
|
+
doc.removeEventListener('ionViewWillLeave', invalidate);
|
|
336
|
+
doc.removeEventListener('visibilitychange', visibility);
|
|
337
|
+
try {
|
|
338
|
+
await opening;
|
|
339
|
+
await restoreWeb();
|
|
340
|
+
} finally {
|
|
341
|
+
try {
|
|
342
|
+
await bounded(plugin.stopOverlays());
|
|
343
|
+
} finally {
|
|
344
|
+
await bounded(listener.remove());
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
};
|