@webview-native-ui/engine 1.0.1 → 1.0.2
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 +14 -0
- package/WebViewNativeUIControls.podspec +17 -0
- package/dist/controls/activation.d.ts +15 -0
- package/dist/controls/activation.js +370 -0
- package/dist/controls/components/index.d.ts +16 -0
- package/dist/controls/components/index.js +38 -0
- package/dist/controls/components/ion-back-button.d.ts +4 -0
- package/dist/controls/components/ion-back-button.js +16 -0
- package/dist/controls/components/ion-button.d.ts +4 -0
- package/dist/controls/components/ion-button.js +26 -0
- package/dist/controls/components/ion-buttons.d.ts +5 -0
- package/dist/controls/components/ion-buttons.js +34 -0
- package/dist/controls/components/ion-fab.d.ts +4 -0
- package/dist/controls/components/ion-fab.js +164 -0
- package/dist/controls/components/ion-menu-button.d.ts +7 -0
- package/dist/controls/components/ion-menu-button.js +41 -0
- package/dist/controls/components/ion-segment.d.ts +5 -0
- package/dist/controls/components/ion-segment.js +24 -0
- package/dist/controls/components/ion-tab-bar.d.ts +5 -0
- package/dist/controls/components/ion-tab-bar.js +49 -0
- package/dist/controls/components/searchable-tabs.d.ts +20 -0
- package/dist/controls/components/searchable-tabs.js +525 -0
- package/dist/controls/definitions.d.ts +360 -0
- package/dist/controls/definitions.js +1 -0
- package/dist/controls/extensions.d.ts +44 -0
- package/dist/controls/extensions.js +164 -0
- package/dist/controls/foldable/fold-state.d.ts +4 -0
- package/dist/controls/foldable/fold-state.js +8 -0
- package/dist/controls/foldable/types.d.ts +9 -0
- package/dist/controls/foldable/types.js +1 -0
- package/dist/controls/html.d.ts +8 -0
- package/dist/controls/html.js +260 -0
- package/dist/controls/index.d.ts +7 -0
- package/dist/controls/index.js +4 -0
- package/dist/controls/ionic.d.ts +1 -0
- package/dist/controls/ionic.js +1 -0
- package/dist/controls/legacy-layout.d.ts +7 -0
- package/dist/controls/legacy-layout.js +23 -0
- package/dist/controls/overlays/content.d.ts +2 -0
- package/dist/controls/overlays/content.js +55 -0
- package/dist/controls/overlays/controller.d.ts +7 -0
- package/dist/controls/overlays/controller.js +369 -0
- package/dist/controls/overlays/covered-overlay.d.ts +4 -0
- package/dist/controls/overlays/covered-overlay.js +9 -0
- package/dist/controls/overlays/dialog-animation.d.ts +7 -0
- package/dist/controls/overlays/dialog-animation.js +49 -0
- package/dist/controls/overlays/dismiss-animation.d.ts +2 -0
- package/dist/controls/overlays/dismiss-animation.js +14 -0
- package/dist/controls/overlays/focus.d.ts +2 -0
- package/dist/controls/overlays/focus.js +67 -0
- package/dist/controls/overlays/popover.d.ts +10 -0
- package/dist/controls/overlays/popover.js +117 -0
- package/dist/controls/overlays/styles.d.ts +9 -0
- package/dist/controls/overlays/styles.js +194 -0
- package/dist/controls/prehide.d.ts +6 -0
- package/dist/controls/prehide.js +280 -0
- package/dist/controls/runtime.d.ts +5 -0
- package/dist/controls/runtime.js +1685 -0
- package/dist/controls/shared/bridge.d.ts +4 -0
- package/dist/controls/shared/bridge.js +15 -0
- package/dist/controls/shared/candidate.d.ts +23 -0
- package/dist/controls/shared/candidate.js +128 -0
- package/dist/controls/shared/clipping.d.ts +1 -0
- package/dist/controls/shared/clipping.js +1 -0
- package/dist/controls/shared/connection.d.ts +21 -0
- package/dist/controls/shared/connection.js +112 -0
- package/dist/controls/shared/control-options.d.ts +4 -0
- package/dist/controls/shared/control-options.js +14 -0
- package/dist/controls/shared/covered-snapshot.d.ts +35 -0
- package/dist/controls/shared/covered-snapshot.js +41 -0
- package/dist/controls/shared/covered-source.d.ts +22 -0
- package/dist/controls/shared/covered-source.js +72 -0
- package/dist/controls/shared/crossfade.d.ts +11 -0
- package/dist/controls/shared/crossfade.js +71 -0
- package/dist/controls/shared/diagnostics.d.ts +2 -0
- package/dist/controls/shared/diagnostics.js +16 -0
- package/dist/controls/shared/dom.d.ts +45 -0
- package/dist/controls/shared/dom.js +258 -0
- package/dist/controls/shared/events.d.ts +12 -0
- package/dist/controls/shared/events.js +13 -0
- package/dist/controls/shared/horizontal-navigation.d.ts +36 -0
- package/dist/controls/shared/horizontal-navigation.js +90 -0
- package/dist/controls/shared/icons.d.ts +5 -0
- package/dist/controls/shared/icons.js +66 -0
- package/dist/controls/shared/modal-animation.d.ts +21 -0
- package/dist/controls/shared/modal-animation.js +97 -0
- package/dist/controls/shared/modal.d.ts +26 -0
- package/dist/controls/shared/modal.js +261 -0
- package/dist/controls/shared/projection-id.d.ts +2 -0
- package/dist/controls/shared/projection-id.js +2 -0
- package/dist/controls/shared/scroll-placement.d.ts +1 -0
- package/dist/controls/shared/scroll-placement.js +1 -0
- package/dist/controls/shared/tab-bar-width.d.ts +7 -0
- package/dist/controls/shared/tab-bar-width.js +72 -0
- package/dist/controls/shared/toolbar-options.d.ts +10 -0
- package/dist/controls/shared/toolbar-options.js +38 -0
- package/dist/controls/shared/transition-identity.d.ts +3 -0
- package/dist/controls/shared/transition-identity.js +64 -0
- package/dist/controls/shared/vertical-bars-geometry.d.ts +8 -0
- package/dist/controls/shared/vertical-bars-geometry.js +82 -0
- package/dist/controls/shared/vertical-navigation.d.ts +5 -0
- package/dist/controls/shared/vertical-navigation.js +39 -0
- package/dist/controls/startup.d.ts +7 -0
- package/dist/controls/startup.js +72 -0
- package/dist/controls/transport.d.ts +10 -0
- package/dist/controls/transport.js +1 -0
- package/dist/controls/vertical-bars-web-search.d.ts +8 -0
- package/dist/controls/vertical-bars-web-search.js +83 -0
- package/dist/controls/vertical-bars-web.d.ts +2 -0
- package/dist/controls/vertical-bars-web.js +609 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/integration/index.d.ts +36 -0
- package/dist/integration/index.js +25 -0
- package/dist/native/session.js +14 -8
- package/dist/projection/pause.d.ts +10 -0
- package/dist/projection/pause.js +58 -0
- package/dist/projection/styles.d.ts +1 -1
- package/dist/projection/styles.js +0 -1
- package/dist/projection/surfaces.js +2 -1
- package/dist/projection/web.js +39 -5
- package/dist/types.d.ts +5 -1
- package/docs/README.md +1 -0
- package/docs/configuration/README.md +3 -1
- package/docs/configuration/ionic-overlays.md +36 -2
- package/docs/configuration/pause-resume.md +51 -0
- package/docs/controls.md +39 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIButton.swift +98 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIButtons.swift +24 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIComponents.swift +19 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIFab.swift +80 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUISearchController.swift +449 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUISegment.swift +91 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUITabBar.swift +213 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIToolbarTransition.swift +186 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIVerticalBars.swift +696 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIVerticalSearch.swift +117 -0
- package/ios/Sources/WebViewNativeUIControls/Extensions/NativeUI.swift +180 -0
- package/ios/Sources/WebViewNativeUIControls/Extensions/NativeUIRendering.swift +38 -0
- package/ios/Sources/WebViewNativeUIControls/NativeUIController.swift +801 -0
- package/ios/Sources/WebViewNativeUIControls/Overlays/NativeUIOverlayController.swift +131 -0
- package/ios/Sources/WebViewNativeUIControls/Overlays/NativeUIOverlayHost.swift +85 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIControlRendering.swift +21 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUICoveredSnapshots.swift +14 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUICrossfade.swift +35 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIHost.swift +8 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIScrollTracker.swift +2 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUISnapshot.swift +206 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIVerticalBarsGeometry.swift +93 -0
- package/ios/Sources/WebViewNativeUIEngine/CoveredSnapshots.swift +2 -2
- package/ios/Sources/WebViewNativeUIEngine/NativeUIProjection.swift +1 -1
- package/ios/Sources/WebViewNativeUIEngine/Resources/native-ui-engine.js +3 -4
- package/package.json +30 -5
- package/src/controls/activation.ts +427 -0
- package/src/controls/components/index.ts +45 -0
- package/src/controls/components/ion-back-button.ts +22 -0
- package/src/controls/components/ion-button.ts +28 -0
- package/src/controls/components/ion-buttons.ts +37 -0
- package/src/controls/components/ion-fab.ts +151 -0
- package/src/controls/components/ion-menu-button.ts +37 -0
- package/src/controls/components/ion-segment.ts +27 -0
- package/src/controls/components/ion-tab-bar.ts +55 -0
- package/src/controls/components/searchable-tabs.ts +534 -0
- package/src/controls/definitions.ts +338 -0
- package/src/controls/extensions.ts +198 -0
- package/src/controls/foldable/fold-state.ts +10 -0
- package/src/controls/foldable/types.ts +4 -0
- package/src/controls/html.ts +239 -0
- package/src/controls/index.ts +7 -0
- package/src/controls/ionic.ts +1 -0
- package/src/controls/legacy-layout.ts +21 -0
- package/src/controls/overlays/content.ts +51 -0
- package/src/controls/overlays/controller.ts +394 -0
- package/src/controls/overlays/covered-overlay.ts +11 -0
- package/src/controls/overlays/dialog-animation.ts +53 -0
- package/src/controls/overlays/dismiss-animation.ts +15 -0
- package/src/controls/overlays/focus.ts +64 -0
- package/src/controls/overlays/popover.ts +116 -0
- package/src/controls/overlays/styles.ts +195 -0
- package/src/controls/prehide.ts +285 -0
- package/src/controls/runtime.ts +1731 -0
- package/src/controls/shared/bridge.ts +18 -0
- package/src/controls/shared/candidate.ts +163 -0
- package/src/controls/shared/clipping.ts +1 -0
- package/src/controls/shared/connection.ts +105 -0
- package/src/controls/shared/control-options.ts +20 -0
- package/src/controls/shared/covered-snapshot.ts +52 -0
- package/src/controls/shared/covered-source.ts +78 -0
- package/src/controls/shared/crossfade.ts +70 -0
- package/src/controls/shared/diagnostics.ts +13 -0
- package/src/controls/shared/dom.ts +310 -0
- package/src/controls/shared/events.ts +14 -0
- package/src/controls/shared/horizontal-navigation.ts +106 -0
- package/src/controls/shared/icons.ts +64 -0
- package/src/controls/shared/modal-animation.ts +109 -0
- package/src/controls/shared/modal.ts +286 -0
- package/src/controls/shared/projection-id.ts +2 -0
- package/src/controls/shared/scroll-placement.ts +1 -0
- package/src/controls/shared/tab-bar-width.ts +66 -0
- package/src/controls/shared/toolbar-options.ts +51 -0
- package/src/controls/shared/transition-identity.ts +67 -0
- package/src/controls/shared/vertical-bars-geometry.ts +92 -0
- package/src/controls/shared/vertical-navigation.ts +34 -0
- package/src/controls/startup.ts +74 -0
- package/src/controls/transport.ts +11 -0
- package/src/controls/vertical-bars-web-search.ts +86 -0
- package/src/controls/vertical-bars-web.ts +674 -0
- package/src/dom/geometry.ts +24 -0
- package/src/dom/overlays.ts +196 -0
- package/src/dom/scroll-bindings.ts +66 -0
- package/src/dom/scroll-placement.ts +92 -0
- package/src/dom/visibility.ts +29 -0
- package/src/index.ts +19 -0
- package/src/integration/index.ts +64 -0
- package/src/ionic.ts +34 -0
- package/src/native/bootstrap.ts +132 -0
- package/src/native/bridge.ts +18 -0
- package/src/native/resource-url.ts +11 -0
- package/src/native/session.ts +708 -0
- package/src/native/updates.ts +103 -0
- package/src/native/values.ts +29 -0
- package/src/projection/events.ts +58 -0
- package/src/projection/pause.ts +58 -0
- package/src/projection/placement.ts +34 -0
- package/src/projection/scope.ts +13 -0
- package/src/projection/styles.ts +26 -0
- package/src/projection/surface.ts +88 -0
- package/src/projection/surfaces.ts +119 -0
- package/src/projection/web.ts +1013 -0
- package/src/types.ts +58 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
export const sheetText = (sheets: readonly CSSStyleSheet[]): string =>
|
|
2
|
+
sheets.flatMap((sheet) => Array.from(sheet.cssRules, (rule) => rule.cssText)).join('\n');
|
|
3
|
+
|
|
4
|
+
export const relayStyles = (
|
|
5
|
+
source: Document,
|
|
6
|
+
destination: Document,
|
|
7
|
+
overlay: HTMLElement,
|
|
8
|
+
changed?: () => void,
|
|
9
|
+
): { destination: HTMLElement; ready: Promise<void>; stop: () => void } => {
|
|
10
|
+
const base = destination.createElement('base');
|
|
11
|
+
base.href = source.baseURI;
|
|
12
|
+
destination.head.append(base);
|
|
13
|
+
const copies = new Map<Element, Element>();
|
|
14
|
+
const sheets = destination.createElement('style');
|
|
15
|
+
destination.head.append(sheets);
|
|
16
|
+
const sync = () => {
|
|
17
|
+
sheets.textContent = sheetText(source.adoptedStyleSheets);
|
|
18
|
+
const sources = Array.from(source.querySelectorAll('meta[name=viewport],link[rel=stylesheet],style:not([data-overlay-relay-local])'));
|
|
19
|
+
for (const [node, copy] of copies)
|
|
20
|
+
if (!sources.includes(node)) {
|
|
21
|
+
copy.remove();
|
|
22
|
+
copies.delete(node);
|
|
23
|
+
}
|
|
24
|
+
for (const node of sources) {
|
|
25
|
+
let copy = copies.get(node);
|
|
26
|
+
if (!copy) {
|
|
27
|
+
copy = node.cloneNode(true) as Element;
|
|
28
|
+
copies.set(node, copy);
|
|
29
|
+
} else {
|
|
30
|
+
for (const attribute of Array.from(copy.attributes)) copy.removeAttribute(attribute.name);
|
|
31
|
+
for (const attribute of Array.from(node.attributes)) copy.setAttribute(attribute.name, attribute.value);
|
|
32
|
+
copy.textContent = node.textContent;
|
|
33
|
+
}
|
|
34
|
+
destination.head.append(copy);
|
|
35
|
+
}
|
|
36
|
+
destination.head.append(surface);
|
|
37
|
+
};
|
|
38
|
+
const surface = destination.createElement('style');
|
|
39
|
+
surface.textContent = 'html,body{background:transparent!important}';
|
|
40
|
+
const observers: MutationObserver[] = [];
|
|
41
|
+
const mirror = (node: Element, target: HTMLElement, flatten = false) => {
|
|
42
|
+
let attributes = '';
|
|
43
|
+
const update = () => {
|
|
44
|
+
const next = JSON.stringify(Array.from(node.attributes, ({ name, value }) => [name, value]));
|
|
45
|
+
if (next === attributes) return false;
|
|
46
|
+
attributes = next;
|
|
47
|
+
for (const attribute of Array.from(target.attributes)) if (!node.hasAttribute(attribute.name)) target.removeAttribute(attribute.name);
|
|
48
|
+
for (const attribute of Array.from(node.attributes)) target.setAttribute(attribute.name, attribute.value);
|
|
49
|
+
// The popover content must not inherit the source page's rail layout.
|
|
50
|
+
if (target.localName === 'ion-app') target.classList.remove('ios-theme-vertical-bars');
|
|
51
|
+
if (flatten) target.style.setProperty('display', 'contents', 'important');
|
|
52
|
+
return true;
|
|
53
|
+
};
|
|
54
|
+
update();
|
|
55
|
+
const observer = new MutationObserver(() => {
|
|
56
|
+
if (update()) changed?.();
|
|
57
|
+
});
|
|
58
|
+
observer.observe(node, { attributes: true });
|
|
59
|
+
observers.push(observer);
|
|
60
|
+
};
|
|
61
|
+
mirror(source.documentElement, destination.documentElement);
|
|
62
|
+
mirror(source.body, destination.body);
|
|
63
|
+
const ancestors: Element[] = [];
|
|
64
|
+
for (let node = overlay.parentElement; node && node !== source.body; node = node.parentElement) ancestors.unshift(node);
|
|
65
|
+
let parent = destination.body;
|
|
66
|
+
for (const node of ancestors) {
|
|
67
|
+
const shell = destination.createElement(node.localName);
|
|
68
|
+
mirror(node, shell, true);
|
|
69
|
+
parent.append(shell);
|
|
70
|
+
parent = shell;
|
|
71
|
+
}
|
|
72
|
+
sync();
|
|
73
|
+
// A copied external stylesheet can still be loading when Ionic's content is
|
|
74
|
+
// ready. Do not expose an unstyled first native frame (notably button sizes).
|
|
75
|
+
const ready = Promise.all(
|
|
76
|
+
Array.from(destination.querySelectorAll<HTMLLinkElement>('link[rel=stylesheet]'), (link) => {
|
|
77
|
+
if (link.sheet) return Promise.resolve();
|
|
78
|
+
return new Promise<void>((resolve, reject) => {
|
|
79
|
+
link.addEventListener('load', () => resolve(), { once: true });
|
|
80
|
+
link.addEventListener('error', () => reject(new Error('Popover stylesheet unavailable')), { once: true });
|
|
81
|
+
});
|
|
82
|
+
}),
|
|
83
|
+
).then(() => undefined);
|
|
84
|
+
const localStyle = (node: Node) => node instanceof Element && node.matches('style[data-overlay-relay-local]');
|
|
85
|
+
const observer = new MutationObserver((records) => {
|
|
86
|
+
// Source-only projection pixels never affect the relayed content. Their
|
|
87
|
+
// preparation must not dismiss a popover which still owns its native anchor.
|
|
88
|
+
const local = (record: MutationRecord) => {
|
|
89
|
+
const target = record.target instanceof Element ? record.target : record.target.parentElement;
|
|
90
|
+
if (target?.closest('style[data-overlay-relay-local]')) return true;
|
|
91
|
+
const nodes = [...Array.from(record.addedNodes), ...Array.from(record.removedNodes)];
|
|
92
|
+
return record.type === 'childList' && nodes.length > 0 && nodes.every(localStyle);
|
|
93
|
+
};
|
|
94
|
+
if (records.every(local)) return;
|
|
95
|
+
sync();
|
|
96
|
+
changed?.();
|
|
97
|
+
});
|
|
98
|
+
observer.observe(source.head, { subtree: true, childList: true, characterData: true, attributes: true });
|
|
99
|
+
observers.push(observer);
|
|
100
|
+
return { destination: parent, ready, stop: () => observers.forEach((item) => item.disconnect()) };
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Compare the live content at the same proposed size before exposing a native
|
|
104
|
+
// surface. Ancestor/sibling selectors and container queries can change when
|
|
105
|
+
// ancestor shells become display:contents; unsupported contexts stay on Web.
|
|
106
|
+
export const captureRelayAppearance = (roots: Element[]): (() => boolean) => {
|
|
107
|
+
const properties = [
|
|
108
|
+
'display',
|
|
109
|
+
'position',
|
|
110
|
+
'box-sizing',
|
|
111
|
+
'color',
|
|
112
|
+
'background-color',
|
|
113
|
+
'background-image',
|
|
114
|
+
'background-size',
|
|
115
|
+
'background-position',
|
|
116
|
+
'box-shadow',
|
|
117
|
+
'visibility',
|
|
118
|
+
'opacity',
|
|
119
|
+
'transform',
|
|
120
|
+
'filter',
|
|
121
|
+
'overflow',
|
|
122
|
+
'text-decoration',
|
|
123
|
+
'font-family',
|
|
124
|
+
'font-size',
|
|
125
|
+
'font-weight',
|
|
126
|
+
'font-style',
|
|
127
|
+
'line-height',
|
|
128
|
+
'letter-spacing',
|
|
129
|
+
'text-align',
|
|
130
|
+
'text-transform',
|
|
131
|
+
'white-space',
|
|
132
|
+
'direction',
|
|
133
|
+
'padding-top',
|
|
134
|
+
'padding-right',
|
|
135
|
+
'padding-bottom',
|
|
136
|
+
'padding-left',
|
|
137
|
+
'margin-top',
|
|
138
|
+
'margin-right',
|
|
139
|
+
'margin-bottom',
|
|
140
|
+
'margin-left',
|
|
141
|
+
'border-top-width',
|
|
142
|
+
'border-right-width',
|
|
143
|
+
'border-bottom-width',
|
|
144
|
+
'border-left-width',
|
|
145
|
+
'border-top-color',
|
|
146
|
+
'border-radius',
|
|
147
|
+
'flex-direction',
|
|
148
|
+
'flex-wrap',
|
|
149
|
+
'justify-content',
|
|
150
|
+
'align-items',
|
|
151
|
+
'grid-template-columns',
|
|
152
|
+
'grid-template-rows',
|
|
153
|
+
'gap',
|
|
154
|
+
'width',
|
|
155
|
+
'height',
|
|
156
|
+
];
|
|
157
|
+
const snapshots: { node: Element; values: string[] }[] = [];
|
|
158
|
+
const values = (node: Element) => {
|
|
159
|
+
const css = node.ownerDocument.defaultView!.getComputedStyle(node);
|
|
160
|
+
return properties.map((property) => css.getPropertyValue(property));
|
|
161
|
+
};
|
|
162
|
+
const visit = (node: Element) => {
|
|
163
|
+
if (snapshots.length >= 1000) throw new Error('Popover content too complex to verify native styling');
|
|
164
|
+
snapshots.push({ node, values: values(node) });
|
|
165
|
+
Array.from(node.children).forEach(visit);
|
|
166
|
+
if (node.shadowRoot)
|
|
167
|
+
Array.from(node.shadowRoot.children)
|
|
168
|
+
.filter((child) => !child.matches('style, link'))
|
|
169
|
+
.forEach(visit);
|
|
170
|
+
};
|
|
171
|
+
roots.forEach(visit);
|
|
172
|
+
return () =>
|
|
173
|
+
snapshots.every(({ node, values: before }) =>
|
|
174
|
+
values(node).every((value, index) => {
|
|
175
|
+
return value === before[index];
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/** Flattening an ancestor must not remove a containing block or visual effect. */
|
|
181
|
+
export const relayLayoutSupported = (overlay: HTMLElement): boolean => {
|
|
182
|
+
const win = overlay.ownerDocument.defaultView!;
|
|
183
|
+
for (let node = overlay.parentElement; node; node = node.parentElement) {
|
|
184
|
+
const css = win.getComputedStyle(node);
|
|
185
|
+
if (
|
|
186
|
+
css.containerType !== 'normal' ||
|
|
187
|
+
(css.transform !== 'none' && !new DOMMatrixReadOnly(css.transform).isIdentity) ||
|
|
188
|
+
css.perspective !== 'none' ||
|
|
189
|
+
css.filter !== 'none' ||
|
|
190
|
+
css.opacity !== '1'
|
|
191
|
+
)
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
return true;
|
|
195
|
+
};
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { localToolbarOption, toolbarOptions } from './shared/toolbar-options.js';
|
|
2
|
+
import type { VerticalControlAreaOptions } from './definitions.js';
|
|
3
|
+
import { modalUsesVerticalBars } from './shared/modal.js';
|
|
4
|
+
import { LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE, LIFECYCLE_WILL_ENTER, LIFECYCLE_WILL_LEAVE } from './shared/events.js';
|
|
5
|
+
import { VERTICAL_BARS_TRANSITION_CANCELED } from '../integration/index.js';
|
|
6
|
+
import {
|
|
7
|
+
childElements,
|
|
8
|
+
clearVerticalBarsPlacement,
|
|
9
|
+
createVerticalBarsPageState,
|
|
10
|
+
verticalBarsActionCandidate,
|
|
11
|
+
verticalBarsBackCandidate,
|
|
12
|
+
verticalBarsBackWebClass,
|
|
13
|
+
verticalBarsOwned,
|
|
14
|
+
isVerticalBarsToolbarActionShape,
|
|
15
|
+
isPermanentlyExcluded,
|
|
16
|
+
isNativeUIDisabled,
|
|
17
|
+
prehiddenClass,
|
|
18
|
+
prehideRootClass,
|
|
19
|
+
rejectedClass,
|
|
20
|
+
setVerticalBarsPlacement,
|
|
21
|
+
syncToolbarText,
|
|
22
|
+
} from './shared/dom.js';
|
|
23
|
+
|
|
24
|
+
const overlays = 'ion-menu, ion-popover';
|
|
25
|
+
const sourceSelector = 'ion-back-button, ion-menu-button, ion-buttons, ion-button';
|
|
26
|
+
// Realm- and teardown-safe: globals such as Element/HTMLElement may be gone
|
|
27
|
+
// when a queued mutation microtask or a stale listener still runs.
|
|
28
|
+
const isHtmlElement = (node: unknown): node is HTMLElement => !!node && (node as Node).nodeType === 1;
|
|
29
|
+
const asElement = (node: Node | EventTarget | null): HTMLElement | undefined => (isHtmlElement(node) ? node : undefined);
|
|
30
|
+
const backSupported = (element: HTMLElement): boolean => {
|
|
31
|
+
const back = element as HTMLIonBackButtonElement;
|
|
32
|
+
return back.icon === undefined && back.color === undefined && !!back.shadowRoot;
|
|
33
|
+
};
|
|
34
|
+
const eligibleBack = verticalBarsBackCandidate;
|
|
35
|
+
const eligible = verticalBarsActionCandidate;
|
|
36
|
+
const localPlacement = (element: HTMLElement) => localToolbarOption(element, true);
|
|
37
|
+
const selectedAction = (element: HTMLElement, options: VerticalControlAreaOptions): boolean => {
|
|
38
|
+
if (!element.matches('ion-button, ion-menu-button')) return false;
|
|
39
|
+
const explicit = localPlacement(element);
|
|
40
|
+
return explicit ?? (toolbarOptions(options, true).button && isVerticalBarsToolbarActionShape(element));
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Capture toolbar placement once per routed-page epoch, before Ionic paints its transition. */
|
|
44
|
+
export const prehideVerticalBarsToolbarSources = (
|
|
45
|
+
doc: Document,
|
|
46
|
+
enabled: () => boolean = () => true,
|
|
47
|
+
sourceRoot: HTMLElement = doc.body,
|
|
48
|
+
options: VerticalControlAreaOptions = {},
|
|
49
|
+
): { suspend: () => () => void; stop: () => void } => {
|
|
50
|
+
doc.documentElement.classList.toggle(prehideRootClass, enabled());
|
|
51
|
+
const scopes = new Map<HTMLElement, Set<HTMLElement>>();
|
|
52
|
+
const owner = new WeakMap<HTMLElement, HTMLElement>();
|
|
53
|
+
const pendingBacks = new Map<HTMLElement, { scope: HTMLElement; timer: ReturnType<typeof setTimeout> }>();
|
|
54
|
+
const verticalBarsPages = createVerticalBarsPageState();
|
|
55
|
+
const listeners = new AbortController();
|
|
56
|
+
const root = () =>
|
|
57
|
+
sourceRoot.closest<HTMLElement>('ion-app.ios-theme-vertical-bars') ??
|
|
58
|
+
sourceRoot.querySelector<HTMLElement>('ion-app.ios-theme-vertical-bars');
|
|
59
|
+
let suspended = 0;
|
|
60
|
+
let stopped = false;
|
|
61
|
+
const routedPage = (element: HTMLElement) => element.closest<HTMLElement>('.ion-page:not(ion-app, body)');
|
|
62
|
+
const release = (scope: HTMLElement) => {
|
|
63
|
+
for (const [element, pending] of pendingBacks) {
|
|
64
|
+
if (pending.scope !== scope) continue;
|
|
65
|
+
clearTimeout(pending.timer);
|
|
66
|
+
pendingBacks.delete(element);
|
|
67
|
+
}
|
|
68
|
+
const owned = scopes.get(scope);
|
|
69
|
+
if (!owned) return;
|
|
70
|
+
owned.forEach((element) => {
|
|
71
|
+
if (owner.get(element) !== scope) return;
|
|
72
|
+
owner.delete(element);
|
|
73
|
+
element.classList.remove(prehiddenClass);
|
|
74
|
+
element.classList.remove(verticalBarsBackWebClass);
|
|
75
|
+
clearVerticalBarsPlacement(element);
|
|
76
|
+
});
|
|
77
|
+
scopes.delete(scope);
|
|
78
|
+
};
|
|
79
|
+
const capture = (scope: HTMLElement) => {
|
|
80
|
+
if (!root()?.contains(scope) || scope.closest(overlays) || !modalUsesVerticalBars(scope) || verticalBarsPages.isDeparted(scope)) return;
|
|
81
|
+
const owned = scopes.get(scope) ?? new Set<HTMLElement>();
|
|
82
|
+
const place = (element: HTMLElement, rail: boolean) => {
|
|
83
|
+
if (owned.has(element)) return;
|
|
84
|
+
const previousScope = owner.get(element);
|
|
85
|
+
if (previousScope) scopes.get(previousScope)?.delete(element);
|
|
86
|
+
owner.set(element, scope);
|
|
87
|
+
setVerticalBarsPlacement(element, rail);
|
|
88
|
+
if (element.matches('ion-back-button')) element.classList.toggle(verticalBarsBackWebClass, !rail);
|
|
89
|
+
owned.add(element);
|
|
90
|
+
};
|
|
91
|
+
const sources = scope.matches('ion-back-button') ? [scope] : Array.from(scope.querySelectorAll<HTMLElement>(sourceSelector));
|
|
92
|
+
sources.forEach((element) => {
|
|
93
|
+
if (!sourceRoot.contains(element)) return;
|
|
94
|
+
// Shell opt-out is temporary; decide initial placement once it is lifted.
|
|
95
|
+
if (isNativeUIDisabled(element)) return;
|
|
96
|
+
if (!modalUsesVerticalBars(element) || element.closest(overlays) || (scope.matches('.ion-page') && routedPage(element) !== scope))
|
|
97
|
+
return;
|
|
98
|
+
if (element.matches('ion-buttons')) {
|
|
99
|
+
const children = childElements(element);
|
|
100
|
+
const actions = children.filter((child) => eligible(child) && selectedAction(child, options));
|
|
101
|
+
const group =
|
|
102
|
+
(localPlacement(element) ?? toolbarOptions(options, true).buttonGroup) &&
|
|
103
|
+
eligible(element) &&
|
|
104
|
+
actions.length === children.length &&
|
|
105
|
+
(actions.length > 1 || (actions.length === 1 && actions[0].matches('ion-menu-button')));
|
|
106
|
+
const freshGroup = !owned.has(element);
|
|
107
|
+
place(element, group);
|
|
108
|
+
children.forEach((child) => {
|
|
109
|
+
if (!child.matches('ion-back-button')) place(child, freshGroup && actions.includes(child));
|
|
110
|
+
});
|
|
111
|
+
} else if (element.matches('ion-back-button') || !element.parentElement?.matches('ion-buttons')) {
|
|
112
|
+
if (owned.has(element)) return;
|
|
113
|
+
if (
|
|
114
|
+
element.matches('ion-back-button') &&
|
|
115
|
+
eligibleBack(element) &&
|
|
116
|
+
(element as HTMLIonBackButtonElement).icon === undefined &&
|
|
117
|
+
(element as HTMLIonBackButtonElement).color === undefined &&
|
|
118
|
+
!element.shadowRoot &&
|
|
119
|
+
!element.classList.contains('hydrated')
|
|
120
|
+
) {
|
|
121
|
+
const previousPending = pendingBacks.get(element);
|
|
122
|
+
if (previousPending && previousPending.scope !== scope) {
|
|
123
|
+
clearTimeout(previousPending.timer);
|
|
124
|
+
pendingBacks.delete(element);
|
|
125
|
+
}
|
|
126
|
+
if (!pendingBacks.has(element)) {
|
|
127
|
+
const timer = setTimeout(() => {
|
|
128
|
+
if (pendingBacks.get(element)?.scope !== scope) return;
|
|
129
|
+
pendingBacks.delete(element);
|
|
130
|
+
// A custom element that never hydrates must remain usable on Web.
|
|
131
|
+
place(element, false);
|
|
132
|
+
reconcile();
|
|
133
|
+
}, 1500);
|
|
134
|
+
pendingBacks.set(element, { scope, timer });
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const pending = pendingBacks.get(element);
|
|
139
|
+
if (pending) {
|
|
140
|
+
clearTimeout(pending.timer);
|
|
141
|
+
pendingBacks.delete(element);
|
|
142
|
+
}
|
|
143
|
+
place(
|
|
144
|
+
element,
|
|
145
|
+
element.matches('ion-back-button')
|
|
146
|
+
? (localPlacement(element) ?? toolbarOptions(options, true).backButton) && eligibleBack(element) && backSupported(element)
|
|
147
|
+
: eligible(element) && selectedAction(element, options),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
scopes.set(scope, owned);
|
|
152
|
+
};
|
|
153
|
+
const reconcile = () => {
|
|
154
|
+
doc.documentElement.classList.toggle(prehideRootClass, enabled() && !suspended);
|
|
155
|
+
syncToolbarText(doc);
|
|
156
|
+
const verticalBars = root();
|
|
157
|
+
if (!verticalBars) {
|
|
158
|
+
Array.from(scopes.keys()).forEach(release);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
for (const scope of scopes.keys()) if (!scope.isConnected || !modalUsesVerticalBars(scope)) release(scope);
|
|
162
|
+
for (const [element, pending] of pendingBacks) if (element.shadowRoot || element.classList.contains('hydrated')) capture(pending.scope);
|
|
163
|
+
// Ionic inserts the destination as invisible before WillEnter. Hide its
|
|
164
|
+
// sources in that same mutation microtask; capture new DOM identities on active pages too.
|
|
165
|
+
verticalBars.querySelectorAll<HTMLElement>('.ion-page:not(ion-app, body, .ion-page-hidden)').forEach(capture);
|
|
166
|
+
// Root toolbars can mount after startup; each new DOM identity is captured once.
|
|
167
|
+
verticalBars.querySelectorAll<HTMLElement>(sourceSelector).forEach((element) => {
|
|
168
|
+
if (routedPage(element) || element.closest(overlays)) return;
|
|
169
|
+
const scope = element.closest<HTMLElement>('ion-toolbar') ?? (element.matches('ion-back-button') ? element : null);
|
|
170
|
+
if (scope) capture(scope);
|
|
171
|
+
});
|
|
172
|
+
for (const owned of scopes.values()) {
|
|
173
|
+
for (const element of owned) {
|
|
174
|
+
const groupOwnsChildren =
|
|
175
|
+
element.parentElement?.matches('ion-buttons') &&
|
|
176
|
+
verticalBarsOwned(element.parentElement) &&
|
|
177
|
+
childElements(element.parentElement).every((child) => verticalBarsOwned(child));
|
|
178
|
+
const hide =
|
|
179
|
+
enabled() &&
|
|
180
|
+
!element.closest('[data-native-ui-placement="element"]') &&
|
|
181
|
+
!suspended &&
|
|
182
|
+
verticalBarsOwned(element) &&
|
|
183
|
+
!groupOwnsChildren &&
|
|
184
|
+
element.isConnected &&
|
|
185
|
+
!element.closest(`.${rejectedClass}`) &&
|
|
186
|
+
!isPermanentlyExcluded(element) &&
|
|
187
|
+
!isNativeUIDisabled(element) &&
|
|
188
|
+
(!element.matches('ion-back-button') || backSupported(element) || !element.classList.contains('hydrated')) &&
|
|
189
|
+
(!element.matches('ion-buttons') || childElements(element).every((child) => verticalBarsOwned(child)));
|
|
190
|
+
element.classList.toggle(prehiddenClass, hide);
|
|
191
|
+
if (element.matches('ion-back-button')) element.classList.toggle(verticalBarsBackWebClass, !hide);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
const onWillEnter = (event: Event) => {
|
|
196
|
+
const page = asElement(event.target);
|
|
197
|
+
if (page?.matches('.ion-page')) {
|
|
198
|
+
verticalBarsPages.lifecycle(event);
|
|
199
|
+
release(page); // Cached pages begin a new placement epoch.
|
|
200
|
+
capture(page);
|
|
201
|
+
reconcile();
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
const onPageLifecycle: EventListener = (event) => {
|
|
205
|
+
verticalBarsPages.lifecycle(event);
|
|
206
|
+
if (event.type !== LIFECYCLE_DID_LEAVE) return;
|
|
207
|
+
const page = asElement(event.target);
|
|
208
|
+
if (page) release(page);
|
|
209
|
+
};
|
|
210
|
+
// A canceled transition keeps the leaving page active and abandons the entering one.
|
|
211
|
+
const onTransitionCanceled: EventListener = (event) => {
|
|
212
|
+
const leaving = asElement(event.target);
|
|
213
|
+
if (!leaving) return;
|
|
214
|
+
const entering = (event as CustomEvent<{ entering?: HTMLElement }>).detail?.entering;
|
|
215
|
+
verticalBarsPages.cancel(entering, leaving);
|
|
216
|
+
reconcile();
|
|
217
|
+
};
|
|
218
|
+
doc.addEventListener(LIFECYCLE_WILL_ENTER, onWillEnter, { capture: true, signal: listeners.signal });
|
|
219
|
+
for (const name of [LIFECYCLE_WILL_LEAVE, LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE])
|
|
220
|
+
doc.addEventListener(name, onPageLifecycle, { capture: true, signal: listeners.signal });
|
|
221
|
+
doc.addEventListener(VERTICAL_BARS_TRANSITION_CANCELED, onTransitionCanceled, { capture: true, signal: listeners.signal });
|
|
222
|
+
doc.defaultView?.addEventListener('nativeUIRefresh', reconcile, { signal: listeners.signal });
|
|
223
|
+
reconcile();
|
|
224
|
+
const mutationRelevant = (record: MutationRecord) => {
|
|
225
|
+
if (record.type === 'characterData') return record.target.parentElement?.matches('ion-toolbar') === true;
|
|
226
|
+
if (record.type === 'childList' && asElement(record.target)?.matches('ion-toolbar')) return true;
|
|
227
|
+
if (record.type === 'childList')
|
|
228
|
+
return [...Array.from(record.addedNodes), ...Array.from(record.removedNodes)].some(
|
|
229
|
+
(node) =>
|
|
230
|
+
isHtmlElement(node) &&
|
|
231
|
+
(node.matches(`${sourceSelector}, .ion-page, ion-toolbar`) || !!node.querySelector(`${sourceSelector}, .ion-page, ion-toolbar`)),
|
|
232
|
+
);
|
|
233
|
+
if (record.attributeName !== 'class') return true;
|
|
234
|
+
const current = (record.target as Element).className;
|
|
235
|
+
const previous = record.oldValue ?? '';
|
|
236
|
+
const withoutOwned = (value: string) =>
|
|
237
|
+
value
|
|
238
|
+
.split(/\s+/)
|
|
239
|
+
.filter((name) => name && ![prehiddenClass, prehideRootClass, verticalBarsBackWebClass].includes(name))
|
|
240
|
+
.join(' ');
|
|
241
|
+
return typeof current !== 'string' || withoutOwned(previous) !== withoutOwned(current);
|
|
242
|
+
};
|
|
243
|
+
let scheduled = false;
|
|
244
|
+
const observer = new (doc.defaultView?.MutationObserver ?? MutationObserver)((records) => {
|
|
245
|
+
if (scheduled || stopped || !records.some(mutationRelevant)) return;
|
|
246
|
+
scheduled = true;
|
|
247
|
+
queueMicrotask(() => {
|
|
248
|
+
scheduled = false;
|
|
249
|
+
if (!stopped) reconcile();
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
observer.observe(doc.documentElement, {
|
|
253
|
+
subtree: true,
|
|
254
|
+
childList: true,
|
|
255
|
+
characterData: true,
|
|
256
|
+
attributes: true,
|
|
257
|
+
attributeOldValue: true,
|
|
258
|
+
attributeFilter: ['class', 'data-native-ui', 'hidden', 'inert', 'icon', 'color'],
|
|
259
|
+
});
|
|
260
|
+
return {
|
|
261
|
+
suspend: () => {
|
|
262
|
+
if (stopped) return () => {};
|
|
263
|
+
suspended++;
|
|
264
|
+
doc.documentElement.classList.remove(prehideRootClass);
|
|
265
|
+
reconcile();
|
|
266
|
+
let resumed = false;
|
|
267
|
+
return () => {
|
|
268
|
+
if (resumed || stopped) return;
|
|
269
|
+
resumed = true;
|
|
270
|
+
if (--suspended === 0) {
|
|
271
|
+
doc.documentElement.classList.add(prehideRootClass);
|
|
272
|
+
reconcile();
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
},
|
|
276
|
+
stop: () => {
|
|
277
|
+
if (stopped) return;
|
|
278
|
+
stopped = true;
|
|
279
|
+
observer.disconnect();
|
|
280
|
+
listeners.abort();
|
|
281
|
+
doc.documentElement.classList.remove(prehideRootClass);
|
|
282
|
+
Array.from(scopes.keys()).forEach(release);
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
};
|