@webview-native-ui/engine 1.0.0 → 1.0.2-0
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 +48 -35
- 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/bootstrap.js +1 -1
- 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 +11 -8
- package/dist/projection/web.js +48 -13
- package/dist/types.d.ts +5 -1
- package/docs/README.md +17 -10
- package/docs/concepts.md +39 -14
- package/docs/configuration/README.md +4 -2
- package/docs/configuration/ionic-overlays.md +36 -2
- package/docs/configuration/pause-resume.md +51 -0
- package/docs/controls.md +39 -0
- package/docs/getting-started/capacitor.md +3 -3
- package/docs/getting-started/cordova.md +3 -1
- package/docs/getting-started/initialization.md +18 -2
- package/docs/reference/platforms.md +2 -2
- package/docs/reference/projection.md +1 -1
- 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 +6 -4
- package/ios/Sources/WebViewNativeUIEngine/ProjectionOrigins.swift +31 -0
- 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,1685 @@
|
|
|
1
|
+
import { createNativeUpdateQueue, createScrollBindings, createWebProjectionSurfaces, } from '../index.js';
|
|
2
|
+
import { enableSupportIonicOverlays, coveredIonicSurface } from '../ionic.js';
|
|
3
|
+
import { movingModal, modalMotionEvent } from './shared/modal.js';
|
|
4
|
+
import { coverOverlayBackground, usesCoveredBackground } from './overlays/covered-overlay.js';
|
|
5
|
+
import { getProjectionState } from './shared/covered-source.js';
|
|
6
|
+
import { createCoveredSnapshots } from './shared/covered-snapshot.js';
|
|
7
|
+
import { createTabBarWidths } from './shared/tab-bar-width.js';
|
|
8
|
+
import { createVerticalNavigation } from './shared/vertical-navigation.js';
|
|
9
|
+
import { createHorizontalNavigation, horizontalToolbar, navigationMarker } from './shared/horizontal-navigation.js';
|
|
10
|
+
import { assignToolbarTransitionIds } from './shared/transition-identity.js';
|
|
11
|
+
import { controlSelected } from './shared/control-options.js';
|
|
12
|
+
import { createDiagnostics } from './shared/diagnostics.js';
|
|
13
|
+
import { htmlSelector, readHTMLControl } from './html.js';
|
|
14
|
+
import { extensionAncestors, readExtension } from './extensions.js';
|
|
15
|
+
import { bounded, boundedListener } from './shared/bridge.js';
|
|
16
|
+
import { alignVerticalTabBar, forgetVerticalBarFrame, rememberVerticalBarFrame, validVerticalBarFrame, } from './shared/vertical-bars-geometry.js';
|
|
17
|
+
import { projectionIds } from './shared/projection-id.js';
|
|
18
|
+
import { inVerticalBarsSurface, topModal, modalStack, modalUsesVerticalBars, modalVerticalBarFrame, modalPreparing, modalDismissing, modalPresentation, } from './shared/modal.js';
|
|
19
|
+
import { LIFECYCLE_WILL_ENTER, LIFECYCLE_WILL_LEAVE, LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE } from './shared/events.js';
|
|
20
|
+
import { VERTICAL_BARS_TRANSITION_CANCELED, getNativeSearchBindings, setNativeUIIntegration } from '../integration/index.js';
|
|
21
|
+
import { createSearchSupport } from './components/searchable-tabs.js';
|
|
22
|
+
import { readCandidate, selector, shadowSelector, motionSelector, isVerticalBarsCandidate } from './components/index.js';
|
|
23
|
+
import { activateProjectedElement, inFixedToolbar, createVerticalBarsPageState, isVerticalBarsSource, preferredVerticalBarsBack, marker, syncToolbarText, toolbarTextMarker, prehideOnlyMutation, rejectedClass, unprojected, } from './shared/dom.js';
|
|
24
|
+
import { createIconRenderer } from './shared/icons.js';
|
|
25
|
+
import { CSS_MOTION_EVENTS } from './shared/events.js';
|
|
26
|
+
import { createCrossfade, fadeMarker } from './shared/crossfade.js';
|
|
27
|
+
// Only these Web overlays leave the rail's display and interaction intact.
|
|
28
|
+
const railPreservingOverlays = 'ion-toast, ion-popover, ion-picker';
|
|
29
|
+
const verticalBarsMarker = 'data-native-ui-vertical-bars';
|
|
30
|
+
const verticalBarsMemberMarker = 'data-native-ui-vertical-bars-member';
|
|
31
|
+
export const createRuntime = async (doc, plugin, options = {}, nativeVerticalBars = () => true, onStop, connection) => {
|
|
32
|
+
const projectionState = getProjectionState(doc);
|
|
33
|
+
const { isProjectionMutation, sourcesChangedEvent } = projectionState;
|
|
34
|
+
const win = doc.defaultView;
|
|
35
|
+
const extensions = options.extensions ?? [];
|
|
36
|
+
const diagnose = createDiagnostics(options.debug);
|
|
37
|
+
const scope = options.root ?? doc;
|
|
38
|
+
const discovery = [
|
|
39
|
+
options.source === 'extensions'
|
|
40
|
+
? ':not(*)'
|
|
41
|
+
: options.source === 'html'
|
|
42
|
+
? htmlSelector
|
|
43
|
+
: options.source === 'auto'
|
|
44
|
+
? `${selector}, ${htmlSelector}`
|
|
45
|
+
: selector,
|
|
46
|
+
...extensions.map((extension) => extension.selector),
|
|
47
|
+
].join(', ');
|
|
48
|
+
const icons = createIconRenderer();
|
|
49
|
+
const crossfade = createCrossfade(win);
|
|
50
|
+
const projectionSurfaces = createWebProjectionSurfaces({
|
|
51
|
+
ignoreTarget: (target) => !!target.closest('.ion-cloned-element, [data-native-ui-proxy]'),
|
|
52
|
+
});
|
|
53
|
+
const imagesFor = (modal) => projectionSurfaces.get(modal ?? doc);
|
|
54
|
+
const coveredSurface = (overlay) => coveredIonicSurface(overlay);
|
|
55
|
+
const ids = new WeakMap();
|
|
56
|
+
let rejected = new WeakMap();
|
|
57
|
+
const sources = new Map();
|
|
58
|
+
let acquiringSources;
|
|
59
|
+
const unregisterNativeOwner = projectionState.registerNativeOwner((source) => sources.has(source) || acquiringSources?.has(source) === true);
|
|
60
|
+
const verticalBarsOwners = new Set();
|
|
61
|
+
const verticalBarsMembers = new Set();
|
|
62
|
+
const suspended = new Set();
|
|
63
|
+
const navigation = createHorizontalNavigation();
|
|
64
|
+
const verticalNavigation = createVerticalNavigation();
|
|
65
|
+
let verticalGesture;
|
|
66
|
+
let acknowledged = [];
|
|
67
|
+
let nativeScrollOwners = new Set();
|
|
68
|
+
const scrollBindings = createScrollBindings();
|
|
69
|
+
const reusesScroll = (candidate) => scrollBindings.reuse(candidate, win.innerWidth);
|
|
70
|
+
const scrollControl = (candidate) => candidate.control.scroll
|
|
71
|
+
? { ...candidate.control, scroll: { ...candidate.control.scroll, reuse: reusesScroll(candidate) } }
|
|
72
|
+
: candidate.control;
|
|
73
|
+
const pages = new Set();
|
|
74
|
+
const tabActivationPages = new Map();
|
|
75
|
+
const verticalBarsPages = createVerticalBarsPageState();
|
|
76
|
+
const overlayMonitor = enableSupportIonicOverlays(undefined, doc);
|
|
77
|
+
const presented = overlayMonitor.active;
|
|
78
|
+
const blockingRailOverlays = new WeakSet();
|
|
79
|
+
const manualSuspensions = new Map();
|
|
80
|
+
const moving = new Map();
|
|
81
|
+
const observed = new Set();
|
|
82
|
+
const listeners = new AbortController();
|
|
83
|
+
let actions = new Map();
|
|
84
|
+
let nextId = 0;
|
|
85
|
+
let revision = 0;
|
|
86
|
+
const tabGeometry = new Map();
|
|
87
|
+
const tabWidths = createTabBarWidths(doc);
|
|
88
|
+
let acceptedRevision = 0;
|
|
89
|
+
let updatingRevision;
|
|
90
|
+
let pendingActivations = [];
|
|
91
|
+
let lastSequence = 0;
|
|
92
|
+
let lastTabSequence = 0;
|
|
93
|
+
let lastSnapshot = '';
|
|
94
|
+
let playingModalAnimation;
|
|
95
|
+
let renderedSnapshot;
|
|
96
|
+
let interruptAssets;
|
|
97
|
+
let viewport = `${win.innerWidth}:${win.innerHeight}`;
|
|
98
|
+
let forceRefresh = false;
|
|
99
|
+
let lastUpdateStarted = 0;
|
|
100
|
+
let connectionSession;
|
|
101
|
+
let stopConnectionCheck;
|
|
102
|
+
let stopped = false;
|
|
103
|
+
let destruction;
|
|
104
|
+
const retentions = new Set();
|
|
105
|
+
/** True while a tab-switch handoff should skip crossfade. */
|
|
106
|
+
let tabSwitchHandoff = false;
|
|
107
|
+
/** Keep instant updates until this time — DidLeave often precedes the retire sync. */
|
|
108
|
+
let handoffUntil = 0;
|
|
109
|
+
/** Tab switch armed while a sync was already in flight; extend instant past that sync. */
|
|
110
|
+
let handoffAcrossPending = false;
|
|
111
|
+
/** Captured at the start of each sync so an in-flight update keeps a stable duration. */
|
|
112
|
+
let handoffInstant = false;
|
|
113
|
+
/** Retire native controls immediately as a modal starts covering the page. */
|
|
114
|
+
let modalHandoff = false;
|
|
115
|
+
let updates = 0;
|
|
116
|
+
let reason;
|
|
117
|
+
let listener;
|
|
118
|
+
let searchListener;
|
|
119
|
+
let waiters = [];
|
|
120
|
+
const controlEnabled = (candidate) => {
|
|
121
|
+
const kind = candidate.control.kind;
|
|
122
|
+
// Button readers apply selection/grouping per child; explicit HTML and extensions
|
|
123
|
+
// are independent opt-ins. Only other automatic kinds need this final selection.
|
|
124
|
+
const key = kind === 'tab-bar' ? 'tabs' : kind === 'segment' ? 'segment' : kind === 'floating-action-button' ? 'fab' : undefined;
|
|
125
|
+
return !key || controlSelected(candidate.element, options, key, candidate.control.placement === 'vertical-bars');
|
|
126
|
+
};
|
|
127
|
+
const id = (element) => {
|
|
128
|
+
let value = ids.get(element);
|
|
129
|
+
if (!value) {
|
|
130
|
+
value = `native-ui-${++nextId}`;
|
|
131
|
+
ids.set(element, value);
|
|
132
|
+
projectionIds.set(element, value);
|
|
133
|
+
}
|
|
134
|
+
return value;
|
|
135
|
+
};
|
|
136
|
+
const style = doc.createElement('style');
|
|
137
|
+
const hidden = `:is([${marker}]:not([${fadeMarker}]), [${navigationMarker}])`;
|
|
138
|
+
style.textContent = `${hidden}, ${hidden} *, ${hidden}::before, ${hidden}::after, ${hidden}::part(native) { visibility: hidden !important; }
|
|
139
|
+
/* Hide the entire clone layer, including its shadow parts, until handoff. */
|
|
140
|
+
.ion-cloned-element:is([${verticalBarsMarker}], [${verticalBarsMemberMarker}]):not([${fadeMarker}]):not([data-native-ui-snapshot-visible]),
|
|
141
|
+
.ion-cloned-element:is([data-native-ui-snapshot-source], [data-native-ui-proxy-prepared]):not([data-native-ui-snapshot-visible]) { opacity: 0 !important; content-visibility: hidden !important; }
|
|
142
|
+
[${marker}], [${marker}] *, [${navigationMarker}], [${navigationMarker}] * { pointer-events: none !important; }
|
|
143
|
+
/* The theme's fixed Back clone must not reintroduce a Web button while
|
|
144
|
+
navigation keeps its source under native ownership. */
|
|
145
|
+
:is(ion-nav, ion-router-outlet):has(.ion-page:not(.ion-page-hidden) ion-back-button:is([${marker}], [${navigationMarker}]))
|
|
146
|
+
> ion-back-button.ion-cloned-element { visibility: hidden !important; }
|
|
147
|
+
/* Ionic disables the covered page while a menu is open. VerticalBars rail
|
|
148
|
+
controls remain outside that page; zero specificity preserves any
|
|
149
|
+
pointer-events rule supplied by the application itself. */
|
|
150
|
+
:where(.menu-content-open) :where([${verticalBarsMarker}]) {
|
|
151
|
+
pointer-events: auto;
|
|
152
|
+
}
|
|
153
|
+
:where(.menu-content-open) :where([${verticalBarsMarker}]) > :where(:not([${verticalBarsMemberMarker}])) {
|
|
154
|
+
pointer-events: none;
|
|
155
|
+
}`;
|
|
156
|
+
const restore = (element, instant = false) => {
|
|
157
|
+
lastSnapshot = '';
|
|
158
|
+
search.release(element);
|
|
159
|
+
element.removeAttribute(marker);
|
|
160
|
+
if (!stopped)
|
|
161
|
+
crossfade.play(element, false, instant ||
|
|
162
|
+
handoffInstant ||
|
|
163
|
+
projectionState.isVisible(element) ||
|
|
164
|
+
(isVerticalBarsSource(element) && !element.matches('ion-tab-bar')));
|
|
165
|
+
if (element.getAttribute('aria-hidden') === 'true') {
|
|
166
|
+
const previous = sources.get(element);
|
|
167
|
+
if (previous == null)
|
|
168
|
+
element.removeAttribute('aria-hidden');
|
|
169
|
+
else
|
|
170
|
+
element.setAttribute('aria-hidden', previous);
|
|
171
|
+
}
|
|
172
|
+
sources.delete(element);
|
|
173
|
+
element.dispatchEvent(new CustomEvent('nativeUIChange'));
|
|
174
|
+
};
|
|
175
|
+
const restoreAll = () => {
|
|
176
|
+
Array.from(sources.keys()).forEach((element) => restore(element));
|
|
177
|
+
verticalBarsOwners.forEach((element) => element.removeAttribute(verticalBarsMarker));
|
|
178
|
+
verticalBarsOwners.clear();
|
|
179
|
+
verticalBarsMembers.forEach((element) => element.removeAttribute(verticalBarsMemberMarker));
|
|
180
|
+
verticalBarsMembers.clear();
|
|
181
|
+
};
|
|
182
|
+
const finishWaiters = (transition) => {
|
|
183
|
+
waiters = waiters.filter((waiter) => {
|
|
184
|
+
if (transition && waiter.transition !== transition)
|
|
185
|
+
return true;
|
|
186
|
+
waiter.resolve();
|
|
187
|
+
return false;
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
const updateQueue = createNativeUpdateQueue(win, {
|
|
191
|
+
update: (send) => sync(send),
|
|
192
|
+
failed: (error) => fail(error),
|
|
193
|
+
retry: () => {
|
|
194
|
+
forceRefresh = true;
|
|
195
|
+
diagnose('sync-retry: refreshing current state after an update failure.');
|
|
196
|
+
},
|
|
197
|
+
reconnect: !!connection,
|
|
198
|
+
});
|
|
199
|
+
const schedule = updateQueue.request;
|
|
200
|
+
const search = createSearchSupport(doc, id, schedule);
|
|
201
|
+
const candidateSources = (candidate) => candidate.sources ?? [candidate.element];
|
|
202
|
+
const setRejected = (element, value) => {
|
|
203
|
+
element.classList.toggle(rejectedClass, value);
|
|
204
|
+
};
|
|
205
|
+
const measuringPointerPages = new WeakSet();
|
|
206
|
+
const readEnabledCandidate = (element) => {
|
|
207
|
+
if (projectionState.owns(element))
|
|
208
|
+
return;
|
|
209
|
+
if (imagesFor() && movingModal(doc) && element.closest('ion-modal') === movingModal(doc) && isVerticalBarsSource(element))
|
|
210
|
+
return;
|
|
211
|
+
if (options.root && !options.root.contains(element))
|
|
212
|
+
return;
|
|
213
|
+
if (element.matches('ion-back-button') && element.closest('ion-app.ios-theme-vertical-bars') && !isVerticalBarsCandidate(element))
|
|
214
|
+
return;
|
|
215
|
+
if (extensions.some((extension) => element.matches(extension.selector))) {
|
|
216
|
+
const extension = extensions.find((definition) => element.matches(definition.selector));
|
|
217
|
+
const unavailable = options.unavailableExtensions?.get(extension.name);
|
|
218
|
+
if (unavailable) {
|
|
219
|
+
diagnose(`${extension.name}: ${unavailable}`, element);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const candidate = readExtension(element, extensions, id, (reason) => diagnose(`${extension.name}: ${reason}`, element), options.nativeScroll);
|
|
223
|
+
if (candidate && !scrollBindings.eligible(candidate, win.innerWidth))
|
|
224
|
+
return undefined; // New bindings are acquired at rest; established bindings keep following.
|
|
225
|
+
return candidate;
|
|
226
|
+
}
|
|
227
|
+
if (options.source === 'extensions')
|
|
228
|
+
return;
|
|
229
|
+
if (options.source === 'html' || element.matches(htmlSelector)) {
|
|
230
|
+
const candidate = readHTMLControl(element, id, options);
|
|
231
|
+
if (!candidate) {
|
|
232
|
+
diagnose('unsupported-control: check the source visibility, accessible name, disabled ancestors, and supported HTML markup.', element);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const rail = candidate.control.placement === 'vertical-bars';
|
|
236
|
+
if (rail && !nativeVerticalBars())
|
|
237
|
+
return;
|
|
238
|
+
return candidate;
|
|
239
|
+
}
|
|
240
|
+
if (element.querySelector(htmlSelector))
|
|
241
|
+
return; // A shared automatic surface must not cover explicitly selected descendants.
|
|
242
|
+
if (options.ionic?.auto === false)
|
|
243
|
+
return;
|
|
244
|
+
if (options.verticalBars?.enabled === false && isVerticalBarsCandidate(element))
|
|
245
|
+
return;
|
|
246
|
+
const pointerPage = isVerticalBarsCandidate(element) ? element.closest('.ion-page') : undefined;
|
|
247
|
+
let candidate;
|
|
248
|
+
if (pointerPage && getComputedStyle(pointerPage).pointerEvents === 'none') {
|
|
249
|
+
const previous = pointerPage.style.getPropertyValue('pointer-events');
|
|
250
|
+
const priority = pointerPage.style.getPropertyPriority('pointer-events');
|
|
251
|
+
const hadStyle = pointerPage.hasAttribute('style');
|
|
252
|
+
measuringPointerPages.add(pointerPage);
|
|
253
|
+
pointerPage.style.setProperty('pointer-events', 'auto', 'important');
|
|
254
|
+
try {
|
|
255
|
+
candidate = readCandidate(element, id, options);
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
if (previous)
|
|
259
|
+
pointerPage.style.setProperty('pointer-events', previous, priority);
|
|
260
|
+
else
|
|
261
|
+
pointerPage.style.removeProperty('pointer-events');
|
|
262
|
+
if (!hadStyle && !pointerPage.style.length)
|
|
263
|
+
pointerPage.removeAttribute('style');
|
|
264
|
+
win.setTimeout(() => measuringPointerPages.delete(pointerPage), 0);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else
|
|
268
|
+
candidate = readCandidate(element, id, options);
|
|
269
|
+
if (candidate && isVerticalBarsCandidate(element) && !element.closest('[data-native-ui-placement="element"]')) {
|
|
270
|
+
if (!nativeVerticalBars())
|
|
271
|
+
return undefined;
|
|
272
|
+
candidate.control.placement = 'vertical-bars';
|
|
273
|
+
if (['button', 'button-group', 'menu-button'].includes(candidate.control.kind)) {
|
|
274
|
+
const slot = (element.matches('ion-buttons') ? element : (element.closest('ion-buttons') ?? element)).getAttribute('slot');
|
|
275
|
+
if (slot === 'start' || slot === 'end')
|
|
276
|
+
candidate.control.toolbarSlot = slot;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (!candidate)
|
|
280
|
+
diagnose('unsupported-control: check the supported Ionic markup, fixed placement, and styling requirements.', element);
|
|
281
|
+
return candidate && controlEnabled(candidate) ? candidate : undefined;
|
|
282
|
+
};
|
|
283
|
+
const flush = async (transition) => {
|
|
284
|
+
if (stopped)
|
|
285
|
+
return;
|
|
286
|
+
schedule();
|
|
287
|
+
// Reconnection owns the next state update; do not block a Web lifecycle on its acknowledgement.
|
|
288
|
+
if (updateQueue.waiting)
|
|
289
|
+
return;
|
|
290
|
+
try {
|
|
291
|
+
await bounded(new Promise((resolve) => waiters.push({ resolve, transition })));
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
await fail(error);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
const blocked = (element) => Array.from(tabActivationPages.keys()).some((page) => page.contains(element)) ||
|
|
298
|
+
verticalBarsPages.isDeparted(element, true) ||
|
|
299
|
+
(!isVerticalBarsSource(element) &&
|
|
300
|
+
(Array.from(suspended).some((scopes) => scopes.some((scope) => scope.contains(element))) ||
|
|
301
|
+
Array.from(pages).some((scope) => scope.contains(element)) ||
|
|
302
|
+
Array.from(moving.keys()).some((surface) => surface.contains(element))));
|
|
303
|
+
const painted = () => new Promise((resolve) => win.requestAnimationFrame(() => win.requestAnimationFrame(() => resolve())));
|
|
304
|
+
const overlayOpen = (includeMenu = true, allowModal = false, ignoredSelector) => {
|
|
305
|
+
const modal = topModal(doc);
|
|
306
|
+
const ignored = (element) => overlayMonitor.isExcluded(element) ||
|
|
307
|
+
(!!ignoredSelector && element.matches(ignoredSelector) && !blockingRailOverlays.has(element)) ||
|
|
308
|
+
(allowModal && !!modal && modalUsesVerticalBars(modal) && element.matches('ion-modal'));
|
|
309
|
+
const presentedOverlayOpen = Array.from(presented).some((element) => !ignored(element) && (includeMenu || !element.matches('ion-menu')));
|
|
310
|
+
return manualSuspensions.size > 0 || presentedOverlayOpen;
|
|
311
|
+
};
|
|
312
|
+
// Compare source data before PNG rendering mutates each item's payload.
|
|
313
|
+
const signature = (candidate, includeTransitionIds = true) => {
|
|
314
|
+
const modal = candidate.element.closest('ion-modal');
|
|
315
|
+
// Native owns placement during modal entry. Translation must not invalidate
|
|
316
|
+
// its acknowledgement, while label, disabled and artwork changes still do.
|
|
317
|
+
const animatedRail = modal && modalPresentation(modal) && candidate.control.placement === 'vertical-bars';
|
|
318
|
+
const control = animatedRail
|
|
319
|
+
? { ...candidate.control, x: 0, y: 0, items: candidate.control.items.map((item) => ({ ...item, x: 0, y: 0 })) }
|
|
320
|
+
: candidate.control;
|
|
321
|
+
return JSON.stringify({ control, icons: candidate.icons.map(({ source }) => source), assets: candidate.assets?.signature }, (key, value) => {
|
|
322
|
+
if (!includeTransitionIds && key === 'transitionId')
|
|
323
|
+
return undefined;
|
|
324
|
+
// Translated DOMRects contain floating-point noise even when the control
|
|
325
|
+
// has not resized. Keep subpixel changes, not numerical round-off.
|
|
326
|
+
return animatedRail && typeof value === 'number' ? Math.round(value * 1000) / 1000 : value;
|
|
327
|
+
});
|
|
328
|
+
};
|
|
329
|
+
const readSources = () => {
|
|
330
|
+
// Ionic owns the closing animation. Keep the covered page on Web until
|
|
331
|
+
// DidDismiss instead of repeatedly projecting the moving modal controls.
|
|
332
|
+
const modal = topModal(doc);
|
|
333
|
+
if (modal && modalDismissing(modal))
|
|
334
|
+
return [];
|
|
335
|
+
if (verticalGesture && (overlayOpen(true, true) || !verticalGesture.scopes.every(inVerticalBarsSurface)))
|
|
336
|
+
verticalGesture = undefined;
|
|
337
|
+
search.keepSearchTabsVisible();
|
|
338
|
+
for (const page of pages)
|
|
339
|
+
if (!page.isConnected)
|
|
340
|
+
pages.delete(page);
|
|
341
|
+
for (const surface of moving.keys())
|
|
342
|
+
if (!surface.isConnected)
|
|
343
|
+
moving.delete(surface);
|
|
344
|
+
if (doc.hidden || overlayOpen(false, true, railPreservingOverlays)) {
|
|
345
|
+
navigation.clear();
|
|
346
|
+
verticalGesture = undefined;
|
|
347
|
+
return [];
|
|
348
|
+
}
|
|
349
|
+
if (win.visualViewport && (win.visualViewport.scale !== 1 || win.visualViewport.offsetTop !== 0) && !search.hasActive()) {
|
|
350
|
+
navigation.clear();
|
|
351
|
+
verticalGesture = undefined;
|
|
352
|
+
return [];
|
|
353
|
+
}
|
|
354
|
+
const overlayVisible = overlayOpen();
|
|
355
|
+
if (overlayVisible)
|
|
356
|
+
navigation.clear();
|
|
357
|
+
// Web rail projections also hide their originals. Measure those sources
|
|
358
|
+
// without releasing the visible clones before native acknowledges ownership.
|
|
359
|
+
const measurableSources = new Set([
|
|
360
|
+
...sources.keys(),
|
|
361
|
+
...Array.from(scope.querySelectorAll('[data-native-ui-snapshot-source]')),
|
|
362
|
+
...Array.from(scope.querySelectorAll(`[${marker}], [data-native-ui-web-source]`)).filter(isVerticalBarsSource),
|
|
363
|
+
]);
|
|
364
|
+
const candidates = navigation.measure(() => unprojected([...measurableSources].filter(inVerticalBarsSurface), () => search
|
|
365
|
+
.decorate(Array.from(scope.querySelectorAll(discovery))
|
|
366
|
+
.filter((element) => inVerticalBarsSurface(element) &&
|
|
367
|
+
!(topModal(doc) && modalPreparing(topModal(doc))) &&
|
|
368
|
+
!verticalBarsPages.isDeparted(element, true) &&
|
|
369
|
+
(!blocked(element) || (overlayVisible && isVerticalBarsSource(element))))
|
|
370
|
+
.map(readEnabledCandidate)
|
|
371
|
+
.filter((candidate) => !!candidate), blocked)
|
|
372
|
+
.filter((candidate) => !rejected.has(candidate.element) || rejected.get(candidate.element) !== signature(candidate, false))));
|
|
373
|
+
// An extension owns its entire source subtree. Never project a nested built-in twice.
|
|
374
|
+
const extensionSources = candidates.filter((candidate) => candidate.control.kind === 'extension');
|
|
375
|
+
const exclusive = candidates.filter((candidate) => {
|
|
376
|
+
if (extensionSources.some((owner) => owner !== candidate && owner.element.contains(candidate.element)))
|
|
377
|
+
return false;
|
|
378
|
+
const tabBarId = candidate.control.extension?.tabBarId;
|
|
379
|
+
if (!tabBarId)
|
|
380
|
+
return true;
|
|
381
|
+
// Wait for the owning native tab host; do not cache an early rejection while its icons load.
|
|
382
|
+
return (candidates.some((owner) => owner.control.id === tabBarId && owner.control.kind === 'tab-bar' && owner.control.placement === 'vertical-bars') && extensionSources.filter((source) => source.control.extension?.tabBarId === tabBarId).length === 1);
|
|
383
|
+
});
|
|
384
|
+
const back = preferredVerticalBarsBack(candidates
|
|
385
|
+
.filter((candidate) => candidate.control.kind === 'back-button' && isVerticalBarsCandidate(candidate.element))
|
|
386
|
+
.map((candidate) => candidate.element), doc);
|
|
387
|
+
const selected = exclusive.filter((candidate) => candidate.control.kind !== 'back-button' || !isVerticalBarsCandidate(candidate.element) || candidate.element === back);
|
|
388
|
+
const result = overlayVisible ? selected.filter((candidate) => isVerticalBarsCandidate(candidate.element)) : selected;
|
|
389
|
+
assignToolbarTransitionIds(result);
|
|
390
|
+
return navigation.retain(result);
|
|
391
|
+
};
|
|
392
|
+
const read = () => tabWidths.measure(readSources);
|
|
393
|
+
const scrollChanged = (event) => {
|
|
394
|
+
const target = event.composedPath()[0];
|
|
395
|
+
if (options.nativeScroll && target instanceof HTMLElement)
|
|
396
|
+
scrollBindings.changed(target);
|
|
397
|
+
projectionSurfaces.changed(target);
|
|
398
|
+
// Only an acknowledged native binding can own motion. Other sources in the
|
|
399
|
+
// same container still need the regular reader (including rejected sources).
|
|
400
|
+
if (target instanceof HTMLElement &&
|
|
401
|
+
nativeScrollOwners.has(target) &&
|
|
402
|
+
!updateQueue.dirty &&
|
|
403
|
+
!projectionSurfaces.restoring &&
|
|
404
|
+
!overlayOpen())
|
|
405
|
+
return;
|
|
406
|
+
schedule();
|
|
407
|
+
};
|
|
408
|
+
const scrollSettled = (event) => {
|
|
409
|
+
if (!options.nativeScroll)
|
|
410
|
+
return;
|
|
411
|
+
const owner = event.composedPath()[0];
|
|
412
|
+
if (owner instanceof HTMLElement) {
|
|
413
|
+
scrollBindings.settled(owner);
|
|
414
|
+
}
|
|
415
|
+
let found = false;
|
|
416
|
+
for (const extension of extensions) {
|
|
417
|
+
if (!extension.scrollElement)
|
|
418
|
+
continue;
|
|
419
|
+
for (const element of Array.from(scope.querySelectorAll(extension.selector))) {
|
|
420
|
+
try {
|
|
421
|
+
if (extension.scrollElement(element) !== owner)
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
continue; // A reader failure keeps the existing Web fallback.
|
|
426
|
+
}
|
|
427
|
+
rejected.delete(element);
|
|
428
|
+
found = true;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
// One settled reconciliation retries temporary binding refusals and notices
|
|
432
|
+
// replaced WebKit scrollers. Reuse normal update/ACK/fallback ownership.
|
|
433
|
+
if (found) {
|
|
434
|
+
lastSnapshot = '';
|
|
435
|
+
schedule();
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
const observe = () => {
|
|
439
|
+
const wanted = new Set();
|
|
440
|
+
for (const element of Array.from(scope.querySelectorAll(discovery))) {
|
|
441
|
+
wanted.add(element);
|
|
442
|
+
if (element.parentElement)
|
|
443
|
+
wanted.add(element.parentElement);
|
|
444
|
+
if (extensions.some((extension) => element.matches(extension.selector))) {
|
|
445
|
+
for (const ancestor of extensionAncestors(element)) {
|
|
446
|
+
wanted.add(ancestor);
|
|
447
|
+
const root = ancestor.getRootNode();
|
|
448
|
+
if (root instanceof ShadowRoot)
|
|
449
|
+
wanted.add(root);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
const inspectShadow = (node) => {
|
|
453
|
+
if (node.shadowRoot) {
|
|
454
|
+
wanted.add(node.shadowRoot);
|
|
455
|
+
node.shadowRoot.querySelectorAll('ion-icon').forEach(inspectShadow);
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
inspectShadow(element);
|
|
459
|
+
element.querySelectorAll(shadowSelector).forEach(inspectShadow);
|
|
460
|
+
}
|
|
461
|
+
// Reconnect only when the set changes; no per-frame observer allocation.
|
|
462
|
+
if (wanted.size === observed.size && Array.from(wanted).every((element) => observed.has(element)))
|
|
463
|
+
return;
|
|
464
|
+
observer.disconnect();
|
|
465
|
+
resize.disconnect();
|
|
466
|
+
for (const element of observed)
|
|
467
|
+
if (element instanceof ShadowRoot) {
|
|
468
|
+
element.removeEventListener('scroll', scrollChanged, true);
|
|
469
|
+
element.removeEventListener('scrollend', scrollSettled, true);
|
|
470
|
+
}
|
|
471
|
+
observed.clear();
|
|
472
|
+
observer.observe(doc.documentElement, observation);
|
|
473
|
+
for (const element of wanted) {
|
|
474
|
+
observed.add(element);
|
|
475
|
+
if (element instanceof ShadowRoot) {
|
|
476
|
+
observer.observe(element, observation);
|
|
477
|
+
on(element, 'scroll', scrollChanged);
|
|
478
|
+
on(element, 'scrollend', scrollSettled);
|
|
479
|
+
}
|
|
480
|
+
else
|
|
481
|
+
resize.observe(element);
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
const fail = async (error) => {
|
|
485
|
+
reason = error instanceof Error ? error.message : String(error);
|
|
486
|
+
console.warn('[Native UI] Returning to Web:', reason);
|
|
487
|
+
const destruction = handle.destroy();
|
|
488
|
+
onStop?.('failure');
|
|
489
|
+
await destruction;
|
|
490
|
+
// The Web fallback re-evaluates eligibility once native projection is stopped.
|
|
491
|
+
win.dispatchEvent(new Event('nativeUIRefresh'));
|
|
492
|
+
};
|
|
493
|
+
const updateNative = async (snapshot, send) => {
|
|
494
|
+
// Native can apply this update even if retention or viewport changes prevent
|
|
495
|
+
// accepting its response. Only a fully accepted snapshot is safe to deduplicate.
|
|
496
|
+
lastSnapshot = '';
|
|
497
|
+
// A handoff can start while an older asynchronous collection is still running.
|
|
498
|
+
if (snapshot.coveredSnapshotToken !== imagesFor()?.token) {
|
|
499
|
+
schedule();
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
lastUpdateStarted = win.performance.now();
|
|
503
|
+
return send(() => bounded(plugin.update(snapshot)), (result) => {
|
|
504
|
+
if (result.revision !== snapshot.revision)
|
|
505
|
+
throw new Error('Native UI revision mismatch');
|
|
506
|
+
});
|
|
507
|
+
};
|
|
508
|
+
const coverWebOverlay = () => {
|
|
509
|
+
if (stopped || retentions.size || manualSuspensions.size)
|
|
510
|
+
return;
|
|
511
|
+
if (![...presented].some((overlay) => overlay.isConnected && !overlayMonitor.isExcluded(overlay) && !overlay.matches('ion-modal') && usesCoveredBackground(overlay)))
|
|
512
|
+
return;
|
|
513
|
+
// Native popovers retain their controls synchronously. Only the Web route reaches here.
|
|
514
|
+
imagesFor(topModal(doc))?.begin();
|
|
515
|
+
};
|
|
516
|
+
const sync = async (send) => {
|
|
517
|
+
followImageMotion();
|
|
518
|
+
for (const modal of projectionSurfaces.keys()) {
|
|
519
|
+
if (modal.isConnected)
|
|
520
|
+
continue;
|
|
521
|
+
void projectionSurfaces.delete(modal);
|
|
522
|
+
projectionSurfaces.end();
|
|
523
|
+
}
|
|
524
|
+
coverWebOverlay();
|
|
525
|
+
if (projectionSurfaces.handoffPending) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
handoffInstant = tabSwitchHandoff || handoffAcrossPending || modalHandoff || win.performance.now() < handoffUntil;
|
|
529
|
+
try {
|
|
530
|
+
// A retained sync performs no update; keep the modal handoff armed so the
|
|
531
|
+
// deferred retirement still runs instantly after the overlay releases.
|
|
532
|
+
if (updateQueue.waiting || (retentions.size && !manualSuspensions.size))
|
|
533
|
+
return;
|
|
534
|
+
const enteringModal = topModal(doc);
|
|
535
|
+
// Geometry refreshes continue for the Web rail during the Ionic animation.
|
|
536
|
+
// Once native has acknowledged retirement, they must not publish it again.
|
|
537
|
+
if (enteringModal && modalDismissing(enteringModal) && renderedSnapshot?.controls.length === 0)
|
|
538
|
+
return;
|
|
539
|
+
if (enteringModal && modalPresentation(enteringModal)?.animation.id === playingModalAnimation && playingModalAnimation !== undefined)
|
|
540
|
+
return;
|
|
541
|
+
modalHandoff = false;
|
|
542
|
+
// A tab is a separate page, not a stack transition. Retire its covers before
|
|
543
|
+
// rendering destination assets, which may continue changing as the page loads.
|
|
544
|
+
// Keep shared surfaces (especially the tab bar and its accessory) native.
|
|
545
|
+
if ((tabSwitchHandoff || handoffAcrossPending || win.performance.now() < handoffUntil) && renderedSnapshot && !navigation.current) {
|
|
546
|
+
const retained = acknowledged.filter((candidate) => candidate.element.isConnected &&
|
|
547
|
+
!verticalBarsPages.isDeparted(candidate.element, true) &&
|
|
548
|
+
!candidate.element.closest('.ion-page-hidden') &&
|
|
549
|
+
!Array.from(tabActivationPages.keys()).some((page) => page.contains(candidate.element)));
|
|
550
|
+
const retiredSearches = new Set(retained
|
|
551
|
+
.filter((candidate) => candidate.control.search?.available &&
|
|
552
|
+
candidateSources(candidate).some((source) => !source.isConnected || verticalBarsPages.isDeparted(source, true) || !!source.closest('.ion-page-hidden')))
|
|
553
|
+
.map((candidate) => candidate.control.id));
|
|
554
|
+
if (retained.length !== acknowledged.length || retiredSearches.size) {
|
|
555
|
+
// The tabs are shared, but their Search belongs to the departing page.
|
|
556
|
+
// Retire it before destination assets without returning its FAB to Web.
|
|
557
|
+
for (const candidate of retained) {
|
|
558
|
+
if (!retiredSearches.has(candidate.control.id))
|
|
559
|
+
continue;
|
|
560
|
+
const configuration = candidate.control.search;
|
|
561
|
+
getNativeSearchBindings(doc)
|
|
562
|
+
.filter((binding) => candidateSources(candidate).includes(binding.footer))
|
|
563
|
+
.forEach((binding) => search.retire(binding, true));
|
|
564
|
+
candidate.control = { ...candidate.control, search: { ...configuration, available: false, active: false, focused: false } };
|
|
565
|
+
candidate.actions.delete(configuration.trigger.id);
|
|
566
|
+
candidate.actions.delete(configuration.closeId);
|
|
567
|
+
}
|
|
568
|
+
const retainedSources = new Set(retained.flatMap(candidateSources));
|
|
569
|
+
for (const element of sources.keys())
|
|
570
|
+
if (!retainedSources.has(element))
|
|
571
|
+
restore(element, true);
|
|
572
|
+
const retirement = {
|
|
573
|
+
...renderedSnapshot,
|
|
574
|
+
revision: ++revision,
|
|
575
|
+
transitionDuration: 0,
|
|
576
|
+
toolbarTransition: undefined,
|
|
577
|
+
verticalBarNavigation: undefined,
|
|
578
|
+
controls: retained.map(scrollControl),
|
|
579
|
+
};
|
|
580
|
+
updatingRevision = retirement.revision;
|
|
581
|
+
updates++;
|
|
582
|
+
const result = await updateNative(retirement, send);
|
|
583
|
+
if (stopped || !result)
|
|
584
|
+
return;
|
|
585
|
+
renderedSnapshot = retirement;
|
|
586
|
+
acceptedRevision = result.revision;
|
|
587
|
+
acknowledged = retained.filter((candidate) => !result.rejectedControls?.includes(candidate.control.id));
|
|
588
|
+
scrollBindings.acknowledge(acknowledged, retirement.viewportWidth);
|
|
589
|
+
for (const candidate of retained) {
|
|
590
|
+
if (acknowledged.includes(candidate))
|
|
591
|
+
continue;
|
|
592
|
+
candidateSources(candidate).forEach((source) => restore(source, true));
|
|
593
|
+
if (candidate.scrollElement)
|
|
594
|
+
nativeScrollOwners.delete(candidate.scrollElement);
|
|
595
|
+
schedule();
|
|
596
|
+
}
|
|
597
|
+
actions = new Map(acknowledged.flatMap((candidate) => Array.from(candidate.actions)));
|
|
598
|
+
lastSnapshot = '';
|
|
599
|
+
const received = pendingActivations;
|
|
600
|
+
pendingActivations = [];
|
|
601
|
+
received.forEach(activate);
|
|
602
|
+
if (tabActivationPages.size)
|
|
603
|
+
finishWaiters();
|
|
604
|
+
if (updateQueue.dirty)
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (options.source !== 'extensions')
|
|
609
|
+
syncToolbarText(doc);
|
|
610
|
+
const size = `${win.innerWidth}:${win.innerHeight}`;
|
|
611
|
+
// WebKit can resize before Ionic's fixed DOM positions catch up.
|
|
612
|
+
// Measure on the next frame instead of retiring a valid native search.
|
|
613
|
+
if (viewport !== size) {
|
|
614
|
+
viewport = size;
|
|
615
|
+
schedule();
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
observe();
|
|
619
|
+
let candidates = read();
|
|
620
|
+
const signatures = new Map(candidates.map((candidate) => [candidate.element, signature(candidate)]));
|
|
621
|
+
const rejectionSignatures = new Map(candidates.map((candidate) => [candidate.element, signature(candidate, false)]));
|
|
622
|
+
const failedAssets = new Set();
|
|
623
|
+
const assets = Promise.all(candidates.map(async (candidate) => {
|
|
624
|
+
try {
|
|
625
|
+
await candidate.assets?.render(icons);
|
|
626
|
+
}
|
|
627
|
+
catch {
|
|
628
|
+
failedAssets.add(candidate);
|
|
629
|
+
diagnose('invalid-assets: image conversion or serialized extension props failed.', candidate.element);
|
|
630
|
+
}
|
|
631
|
+
await Promise.all(candidate.icons.map(async (icon) => {
|
|
632
|
+
const field = icon.field ?? 'icon';
|
|
633
|
+
icon.item[field] = await icons.render(icon.source, field === 'icon' ? icon.item.iconWidth : icon.item.closeIconWidth, field === 'icon' ? icon.item.iconHeight : icon.item.closeIconHeight);
|
|
634
|
+
}));
|
|
635
|
+
}));
|
|
636
|
+
// A tab change invalidates this page. Do not let an in-flight image decode
|
|
637
|
+
// postpone retirement of its native covers.
|
|
638
|
+
await Promise.race([assets, new Promise((resolve) => (interruptAssets = resolve))]);
|
|
639
|
+
interruptAssets = undefined;
|
|
640
|
+
candidates = candidates.filter((candidate) => !failedAssets.has(candidate));
|
|
641
|
+
if (stopped || updateQueue.dirty || (retentions.size && !manualSuspensions.size))
|
|
642
|
+
return;
|
|
643
|
+
const retained = new Set([...candidates.flatMap(candidateSources), ...(verticalGesture?.sources ?? [])]);
|
|
644
|
+
const removed = Array.from(sources.keys()).filter((element) => !retained.has(element));
|
|
645
|
+
if (removed.some(isVerticalBarsSource)) {
|
|
646
|
+
actions.forEach(forgetVerticalBarFrame);
|
|
647
|
+
try {
|
|
648
|
+
const geometry = await bounded(plugin.getVerticalBarsGeometry({ viewportWidth: win.innerWidth, ids: [...actions.keys(), 'BackButton', '__tabBar'] }));
|
|
649
|
+
if (stopped || updateQueue.dirty || (retentions.size && !manualSuspensions.size))
|
|
650
|
+
return;
|
|
651
|
+
if (geometry.revision === acceptedRevision && size === `${win.innerWidth}:${win.innerHeight}`) {
|
|
652
|
+
for (const tabBar of removed.filter((element) => element.matches('ion-tab-bar'))) {
|
|
653
|
+
tabGeometry.get(tabBar)?.();
|
|
654
|
+
tabGeometry.delete(tabBar);
|
|
655
|
+
const items = new Map([...actions]
|
|
656
|
+
.filter(([id, element]) => element.parentElement === tabBar && geometry.items[id] && validVerticalBarFrame(geometry.items[id]))
|
|
657
|
+
.map(([id, element]) => [element, geometry.items[id]]));
|
|
658
|
+
if (geometry.items['__tabBar'] &&
|
|
659
|
+
validVerticalBarFrame(geometry.items['__tabBar']) &&
|
|
660
|
+
items.size === tabBar.querySelectorAll('ion-tab-button').length)
|
|
661
|
+
tabGeometry.set(tabBar, alignVerticalTabBar(tabBar, geometry.items['__tabBar'], items));
|
|
662
|
+
}
|
|
663
|
+
for (const [id, element] of actions) {
|
|
664
|
+
const frame = geometry.items[element.matches('ion-back-button') ? 'BackButton' : id];
|
|
665
|
+
if (frame)
|
|
666
|
+
rememberVerticalBarFrame(element, frame);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
catch {
|
|
671
|
+
/* Older native binaries can still use the CSS fallback. */
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
const committingNavigation = navigation.current?.phase === 'committing';
|
|
675
|
+
const preparingNavigation = navigation.current?.leased && navigation.current.phase === 'playing';
|
|
676
|
+
const returningToWeb = removed.filter((element) => !committingNavigation || !navigation.owns(element));
|
|
677
|
+
if (returningToWeb.length) {
|
|
678
|
+
// Restore the source and let WebKit paint before removing its native cover.
|
|
679
|
+
returningToWeb.forEach((element) => restore(element, preparingNavigation));
|
|
680
|
+
if (removed.some(isVerticalBarsSource))
|
|
681
|
+
win.dispatchEvent(new CustomEvent('nativeUIVerticalBarsRelease', { detail: { instant: handoffInstant } }));
|
|
682
|
+
// The outgoing tab is no longer visible, so waiting two frames only leaves its
|
|
683
|
+
// native snapshot over the destination. Stack transitions still need the paint.
|
|
684
|
+
if (!handoffInstant) {
|
|
685
|
+
await painted();
|
|
686
|
+
if (stopped || updateQueue.dirty)
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
const root = doc.querySelector('ion-app.ios-theme-vertical-bars');
|
|
691
|
+
const verticalBarEdge = options.nativeState?.edge
|
|
692
|
+
? options.nativeState.edge
|
|
693
|
+
: options.rail
|
|
694
|
+
? options.rail.edge
|
|
695
|
+
: root
|
|
696
|
+
? root.classList.contains('ios-theme-vertical-bars-left')
|
|
697
|
+
? 'left'
|
|
698
|
+
: 'right'
|
|
699
|
+
: undefined;
|
|
700
|
+
const modal = topModal(doc);
|
|
701
|
+
const verticalBarFrame = modal && !(imagesFor() && movingModal(doc)) && !modalPreparing(modal) && !modalDismissing(modal) && modalUsesVerticalBars(modal)
|
|
702
|
+
? modalVerticalBarFrame(modal)
|
|
703
|
+
: undefined;
|
|
704
|
+
const transition = navigation.current;
|
|
705
|
+
const railCandidates = candidates.filter((candidate) => candidate.control.placement === 'vertical-bars');
|
|
706
|
+
const activePage = railCandidates.length
|
|
707
|
+
? (railCandidates.find((candidate) => candidate.control.kind !== 'tab-bar')?.element.closest('.ion-page') ??
|
|
708
|
+
Array.from((modal ?? scope).querySelectorAll('.ion-page'))
|
|
709
|
+
.reverse()
|
|
710
|
+
.find((page) => {
|
|
711
|
+
const invisible = page.closest('.ion-page-invisible');
|
|
712
|
+
return (!page.closest('.ion-page-hidden, ion-menu') &&
|
|
713
|
+
!verticalBarsPages.isDeparted(page) &&
|
|
714
|
+
(modal || !page.closest('ion-modal, ion-popover')) &&
|
|
715
|
+
(!invisible || pages.has(invisible)));
|
|
716
|
+
}))
|
|
717
|
+
: undefined;
|
|
718
|
+
const data = {
|
|
719
|
+
coveredSnapshotToken: imagesFor()?.token,
|
|
720
|
+
verticalBarNavigationPhase: verticalGesture ? (verticalGesture.pending ? 'tracking' : 'settling') : undefined,
|
|
721
|
+
verticalBarNavigation: activePage ? verticalNavigation.read(activePage).map(id) : undefined,
|
|
722
|
+
toolbarTransition: transition?.prepared && transition.phase !== 'committing'
|
|
723
|
+
? {
|
|
724
|
+
id: transition.id,
|
|
725
|
+
sourceIds: transition.frozen.map((candidate) => candidate.control.id),
|
|
726
|
+
destination: transition.destination.map((candidate) => candidate.control),
|
|
727
|
+
}
|
|
728
|
+
: undefined,
|
|
729
|
+
verticalBarFrame,
|
|
730
|
+
modalAnimation: modal && verticalBarFrame ? modalPresentation(modal)?.animation : undefined,
|
|
731
|
+
viewportWidth: win.innerWidth,
|
|
732
|
+
verticalBarEdge,
|
|
733
|
+
controls: candidates.map((candidate) => candidate.control),
|
|
734
|
+
};
|
|
735
|
+
const serialized = JSON.stringify(data);
|
|
736
|
+
if (serialized === lastSnapshot && !forceRefresh) {
|
|
737
|
+
search.settle((element) => Array.from(pages).some((page) => page.contains(element)));
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
const acquiringScroll = scrollBindings.acquiring(candidates, win.innerWidth);
|
|
741
|
+
const snapshot = {
|
|
742
|
+
...data,
|
|
743
|
+
controls: candidates.map(scrollControl),
|
|
744
|
+
revision: ++revision,
|
|
745
|
+
transitionDuration: crossfade.duration(handoffInstant || committingNavigation || preparingNavigation || projectionSurfaces.restoring),
|
|
746
|
+
};
|
|
747
|
+
// A native visibility notification during this update must survive its ack.
|
|
748
|
+
forceRefresh = false;
|
|
749
|
+
updates++;
|
|
750
|
+
updatingRevision = snapshot.revision;
|
|
751
|
+
const result = await updateNative(snapshot, send);
|
|
752
|
+
// A provisional binding measured before motion is not an acknowledged
|
|
753
|
+
// coordinate mapping. Keep Web and acquire it again after settling.
|
|
754
|
+
const unconfirmedScroll = scrollBindings.unconfirmed(candidates, acquiringScroll);
|
|
755
|
+
if (unconfirmedScroll.size)
|
|
756
|
+
schedule();
|
|
757
|
+
if (stopped || !result || (retentions.size && !manualSuspensions.size))
|
|
758
|
+
return;
|
|
759
|
+
if (transition && navigation.current === transition && result.toolbarTransition === transition.id) {
|
|
760
|
+
transition.native = true;
|
|
761
|
+
navigation.mask(transition.destination.filter((candidate) => !result.rejectedControls?.includes(candidate.control.id)), true);
|
|
762
|
+
}
|
|
763
|
+
if (transition && navigation.current === transition)
|
|
764
|
+
navigation.unmask(transition.destination.filter((candidate) => result.rejectedControls?.includes(candidate.control.id)));
|
|
765
|
+
if (`${win.innerWidth}:${win.innerHeight}` !== size) {
|
|
766
|
+
lastSnapshot = '';
|
|
767
|
+
schedule();
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
if (result.rejectedSearches?.length) {
|
|
771
|
+
search.reject(result.rejectedSearches);
|
|
772
|
+
lastSnapshot = '';
|
|
773
|
+
schedule();
|
|
774
|
+
}
|
|
775
|
+
if (result.rejectedControls?.some((id) => navigation.current?.frozen.some((candidate) => candidate.control.id === id)))
|
|
776
|
+
navigation.clear();
|
|
777
|
+
for (const candidate of candidates) {
|
|
778
|
+
if (result.rejectedControls?.includes(candidate.control.id)) {
|
|
779
|
+
diagnose('native-rejected: the native renderer rejected this source; check decoded props, placement, and device support.', candidate.element);
|
|
780
|
+
// An established WebKit scroller may be replaced when Web projection
|
|
781
|
+
// styles change. Drop its ACK and reacquire once through the same path;
|
|
782
|
+
// a failed fresh acquisition is cached normally to stop repeated retries.
|
|
783
|
+
if (!snapshot.controls.find((control) => control.id === candidate.control.id)?.scroll?.reuse)
|
|
784
|
+
rejected.set(candidate.element, rejectionSignatures.get(candidate.element));
|
|
785
|
+
else
|
|
786
|
+
candidateSources(candidate).forEach((source) => restore(source, true));
|
|
787
|
+
setRejected(candidate.element, true);
|
|
788
|
+
schedule();
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
// A page mutation must not tear down unchanged shared controls such as tabs.
|
|
792
|
+
const currentCandidates = updateQueue.dirty ? read() : candidates;
|
|
793
|
+
const current = updateQueue.dirty
|
|
794
|
+
? new Map(currentCandidates.map((candidate) => [candidate.element, signature(candidate)]))
|
|
795
|
+
: signatures;
|
|
796
|
+
const currentSources = new Set([...currentCandidates.flatMap(candidateSources), ...(verticalGesture?.sources ?? [])]);
|
|
797
|
+
const accepted = candidates.filter((candidate) => !result.rejectedControls?.includes(candidate.control.id) &&
|
|
798
|
+
current.get(candidate.element) === signatures.get(candidate.element) &&
|
|
799
|
+
(!candidate.scrollElement || !unconfirmedScroll.has(candidate.scrollElement)));
|
|
800
|
+
accepted.forEach((candidate) => setRejected(candidate.element, false));
|
|
801
|
+
const acceptedVerticalBarsOwners = new Set(accepted.filter((candidate) => candidate.control.placement === 'vertical-bars').map((candidate) => candidate.element));
|
|
802
|
+
const acceptedVerticalBarsMembers = new Set(accepted
|
|
803
|
+
.filter((candidate) => candidate.control.placement === 'vertical-bars')
|
|
804
|
+
.flatMap((candidate) => Array.from(candidate.actions.values())));
|
|
805
|
+
const invalidated = candidates.length !== accepted.length;
|
|
806
|
+
renderedSnapshot = snapshot;
|
|
807
|
+
acceptedRevision = result.revision;
|
|
808
|
+
lastSnapshot = invalidated ? '' : serialized;
|
|
809
|
+
actions = new Map(candidates.filter((candidate) => current.has(candidate.element)).flatMap((candidate) => Array.from(candidate.actions)));
|
|
810
|
+
// Keep an existing cover while its content catches up. Only an ineligible
|
|
811
|
+
// source needs to return to Web; new sources still require an exact ack.
|
|
812
|
+
for (const element of sources.keys())
|
|
813
|
+
if (!currentSources.has(element))
|
|
814
|
+
restore(element, committingNavigation && navigation.owns(element));
|
|
815
|
+
for (const element of verticalBarsOwners) {
|
|
816
|
+
if (acceptedVerticalBarsOwners.has(element))
|
|
817
|
+
continue;
|
|
818
|
+
element.removeAttribute(verticalBarsMarker);
|
|
819
|
+
verticalBarsOwners.delete(element);
|
|
820
|
+
}
|
|
821
|
+
for (const element of acceptedVerticalBarsOwners) {
|
|
822
|
+
element.setAttribute(verticalBarsMarker, '');
|
|
823
|
+
verticalBarsOwners.add(element);
|
|
824
|
+
}
|
|
825
|
+
for (const element of verticalBarsMembers) {
|
|
826
|
+
if (acceptedVerticalBarsMembers.has(element))
|
|
827
|
+
continue;
|
|
828
|
+
element.removeAttribute(verticalBarsMemberMarker);
|
|
829
|
+
verticalBarsMembers.delete(element);
|
|
830
|
+
}
|
|
831
|
+
for (const element of acceptedVerticalBarsMembers) {
|
|
832
|
+
element.setAttribute(verticalBarsMemberMarker, '');
|
|
833
|
+
verticalBarsMembers.add(element);
|
|
834
|
+
}
|
|
835
|
+
acquiringSources = new Set(accepted.flatMap(candidateSources));
|
|
836
|
+
if (acceptedVerticalBarsOwners.size)
|
|
837
|
+
win.dispatchEvent(new CustomEvent('nativeUIVerticalBarsAcquire', { detail: { instant: handoffInstant } }));
|
|
838
|
+
for (const candidate of accepted) {
|
|
839
|
+
const width = result.tabBarWidths?.[candidate.control.id];
|
|
840
|
+
if (candidate.control.kind === 'tab-bar' && candidate.control.placement !== 'vertical-bars' && width !== undefined)
|
|
841
|
+
tabWidths.apply(candidate.element, width);
|
|
842
|
+
const instant = handoffInstant ||
|
|
843
|
+
candidateSources(candidate).some(projectionState.isVisible) ||
|
|
844
|
+
(committingNavigation && horizontalToolbar(candidate));
|
|
845
|
+
for (const element of candidateSources(candidate)) {
|
|
846
|
+
const newlyProjected = !sources.has(element) || !element.hasAttribute(marker);
|
|
847
|
+
if (!sources.has(element)) {
|
|
848
|
+
sources.set(element, element.getAttribute('aria-hidden'));
|
|
849
|
+
crossfade.play(element, true, instant || (isVerticalBarsSource(element) && !element.matches('ion-tab-bar')));
|
|
850
|
+
}
|
|
851
|
+
element.setAttribute(marker, '');
|
|
852
|
+
element.setAttribute('aria-hidden', 'true');
|
|
853
|
+
if (newlyProjected)
|
|
854
|
+
element.dispatchEvent(new CustomEvent('nativeUIChange'));
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
acquiringSources = undefined;
|
|
858
|
+
// Transfer DOM ownership before retiring the image; partial/rejected acknowledgements keep it.
|
|
859
|
+
const restoredIDs = accepted
|
|
860
|
+
.filter((candidate) => !result.rejectedControls?.includes(candidate.control.id))
|
|
861
|
+
.map((candidate) => candidate.control.id);
|
|
862
|
+
projectionSurfaces.updated(snapshot.coveredSnapshotToken, restoredIDs, snapshot.revision);
|
|
863
|
+
if (!updateQueue.dirty && !invalidated)
|
|
864
|
+
search.settle((element) => Array.from(pages).some((page) => page.contains(element)));
|
|
865
|
+
// Native has acknowledged this rendered snapshot even when a concurrent DOM
|
|
866
|
+
// mutation invalidates its signature. An existing cover is still visible;
|
|
867
|
+
// keep that snapshot available if navigation starts before the next update.
|
|
868
|
+
acknowledged = candidates.filter((candidate) => !result.rejectedControls?.includes(candidate.control.id) &&
|
|
869
|
+
(!candidate.scrollElement || !unconfirmedScroll.has(candidate.scrollElement)) &&
|
|
870
|
+
candidateSources(candidate).some((source) => sources.has(source)));
|
|
871
|
+
scrollBindings.acknowledge(acknowledged, snapshot.viewportWidth);
|
|
872
|
+
const discovered = Array.from(scope.querySelectorAll(discovery));
|
|
873
|
+
nativeScrollOwners = new Set(acknowledged
|
|
874
|
+
.flatMap((candidate) => (candidate.scrollElement ? [candidate.scrollElement] : []))
|
|
875
|
+
.filter((owner) => discovered.every((element) => !Array.from(extensionAncestors(element)).includes(owner) ||
|
|
876
|
+
extensions.some((extension) => extension.placement === 'tab-accessory' && element.matches(extension.selector)) ||
|
|
877
|
+
acknowledged.some((candidate) => candidate.element === element &&
|
|
878
|
+
(candidate.scrollElement === owner ||
|
|
879
|
+
candidate.control.extension?.tabBarId ||
|
|
880
|
+
candidate.control.placement === 'vertical-bars')))));
|
|
881
|
+
if (snapshot.modalAnimation && !invalidated && !result.rejectedControls?.length && !result.rejectedSearches?.length)
|
|
882
|
+
playingModalAnimation = snapshot.modalAnimation.id;
|
|
883
|
+
if (committingNavigation && !invalidated)
|
|
884
|
+
navigation.clear();
|
|
885
|
+
const received = pendingActivations;
|
|
886
|
+
pendingActivations = [];
|
|
887
|
+
received.forEach(activate);
|
|
888
|
+
// Navigation only needs its acknowledged handoff. Unrelated refreshes and
|
|
889
|
+
// fades may continue without holding the page at its starting position.
|
|
890
|
+
// An earlier lifecycle snapshot may finish after endpoint preparation. It
|
|
891
|
+
// cannot release playback before the prepared transition itself was sent.
|
|
892
|
+
if (preparingNavigation &&
|
|
893
|
+
!invalidated &&
|
|
894
|
+
transition === navigation.current &&
|
|
895
|
+
(!transition?.prepared || snapshot.toolbarTransition?.id === transition.id))
|
|
896
|
+
finishWaiters(transition);
|
|
897
|
+
await crossfade.settled();
|
|
898
|
+
// Geometry belongs to the outgoing tab visual, not to other rail owners.
|
|
899
|
+
// Keep it until that visual has completely faded back to native.
|
|
900
|
+
for (const [tabBar, restore] of tabGeometry) {
|
|
901
|
+
if (!tabBar.isConnected || acceptedVerticalBarsOwners.has(tabBar)) {
|
|
902
|
+
restore();
|
|
903
|
+
tabGeometry.delete(tabBar);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
if (invalidated) {
|
|
907
|
+
schedule();
|
|
908
|
+
// Sources that became ineligible must paint before their cover is removed.
|
|
909
|
+
await painted();
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
catch (error) {
|
|
913
|
+
await fail(error);
|
|
914
|
+
}
|
|
915
|
+
finally {
|
|
916
|
+
updatingRevision = undefined;
|
|
917
|
+
pendingActivations = [];
|
|
918
|
+
// A refresh received before the ack must also invalidate that ack's rejects.
|
|
919
|
+
if (forceRefresh)
|
|
920
|
+
rejected = new WeakMap();
|
|
921
|
+
// A tab switch mid-update (or a dirty follow-up) must keep instant through the retire sync.
|
|
922
|
+
if (!stopped && (handoffAcrossPending || (handoffInstant && updateQueue.dirty))) {
|
|
923
|
+
handoffUntil = Math.max(handoffUntil, win.performance.now() + 400);
|
|
924
|
+
}
|
|
925
|
+
handoffAcrossPending = false;
|
|
926
|
+
if (updateQueue.waiting || !updateQueue.dirty || stopped)
|
|
927
|
+
finishWaiters();
|
|
928
|
+
if (!stopped && !updateQueue.dirty && acceptedRevision === revision && !navigation.current && !verticalGesture) {
|
|
929
|
+
projectionSurfaces.consider(acceptedRevision, acknowledged);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
const observation = {
|
|
934
|
+
subtree: true,
|
|
935
|
+
childList: true,
|
|
936
|
+
characterData: true,
|
|
937
|
+
attributes: true,
|
|
938
|
+
attributeOldValue: true,
|
|
939
|
+
};
|
|
940
|
+
const observer = new MutationObserver((records) => {
|
|
941
|
+
if (records.some((record) => !isProjectionMutation(record) &&
|
|
942
|
+
!(record.attributeName === 'style' &&
|
|
943
|
+
imagesFor() &&
|
|
944
|
+
movingModal(doc) &&
|
|
945
|
+
(record.target === movingModal(doc) || record.target === movingModal(doc)?.shadowRoot?.querySelector('[part~="content"]'))) &&
|
|
946
|
+
record.attributeName !== marker &&
|
|
947
|
+
record.attributeName !== navigationMarker &&
|
|
948
|
+
record.attributeName !== 'data-native-ui-web-source' &&
|
|
949
|
+
record.attributeName !== toolbarTextMarker &&
|
|
950
|
+
record.attributeName !== fadeMarker &&
|
|
951
|
+
!prehideOnlyMutation(record) &&
|
|
952
|
+
!(record.attributeName === 'style' && measuringPointerPages.has(record.target)) &&
|
|
953
|
+
!(record.attributeName === 'aria-hidden' &&
|
|
954
|
+
sources.has(record.target) &&
|
|
955
|
+
record.target.getAttribute('aria-hidden') === 'true'))) {
|
|
956
|
+
projectionSurfaces.changed();
|
|
957
|
+
schedule();
|
|
958
|
+
}
|
|
959
|
+
});
|
|
960
|
+
const resize = new ResizeObserver(schedule);
|
|
961
|
+
const on = (target, name, callback = schedule) => target.addEventListener(name, callback, { capture: true, signal: listeners.signal });
|
|
962
|
+
// Measure only before playback. Restore page attributes synchronously, before WebKit paints.
|
|
963
|
+
const maskNavigationPage = (page) => {
|
|
964
|
+
if (!navigation.current)
|
|
965
|
+
return;
|
|
966
|
+
const className = page.getAttribute('class');
|
|
967
|
+
const pageStyle = page.getAttribute('style');
|
|
968
|
+
// Ionic can hide a transitioning header until its collapse state settles. Its fixed buttons
|
|
969
|
+
// still need correspondence before the transition creates its Web back clone.
|
|
970
|
+
const headers = Array.from(page.querySelectorAll('ion-header.header-transitioning')).filter((header) => !header.closest('ion-content'));
|
|
971
|
+
const headerStyles = headers.map((header) => header.getAttribute('style'));
|
|
972
|
+
headers.forEach((header) => header.style.setProperty('opacity', '1', 'important'));
|
|
973
|
+
measuringPointerPages.add(page);
|
|
974
|
+
page.classList.remove('ion-page-hidden', 'ion-page-invisible');
|
|
975
|
+
page.style.setProperty('pointer-events', 'auto', 'important');
|
|
976
|
+
try {
|
|
977
|
+
const candidates = navigation.measure(() => unprojected(sources.keys(), () =>
|
|
978
|
+
// Only fixed toolbar controls participate in this handoff. Avoid measuring
|
|
979
|
+
// content controls that will be discarded by horizontalToolbar below.
|
|
980
|
+
Array.from(page.querySelectorAll(discovery))
|
|
981
|
+
.filter(inFixedToolbar)
|
|
982
|
+
.map(readEnabledCandidate)
|
|
983
|
+
.filter((candidate) => !!candidate && horizontalToolbar(candidate))));
|
|
984
|
+
assignToolbarTransitionIds(candidates);
|
|
985
|
+
navigation.current.destination = candidates;
|
|
986
|
+
navigation.mask(candidates);
|
|
987
|
+
}
|
|
988
|
+
finally {
|
|
989
|
+
headers.forEach((header, index) => {
|
|
990
|
+
const style = headerStyles[index];
|
|
991
|
+
if (style === null)
|
|
992
|
+
header.removeAttribute('style');
|
|
993
|
+
else
|
|
994
|
+
header.setAttribute('style', style);
|
|
995
|
+
});
|
|
996
|
+
if (className === null)
|
|
997
|
+
page.removeAttribute('class');
|
|
998
|
+
else
|
|
999
|
+
page.setAttribute('class', className);
|
|
1000
|
+
if (pageStyle === null)
|
|
1001
|
+
page.removeAttribute('style');
|
|
1002
|
+
else
|
|
1003
|
+
page.setAttribute('style', pageStyle);
|
|
1004
|
+
win.setTimeout(() => measuringPointerPages.delete(page), 0);
|
|
1005
|
+
}
|
|
1006
|
+
};
|
|
1007
|
+
const finishNavigation = (transition = navigation.current) => {
|
|
1008
|
+
if (transition !== navigation.current || !transition || transition.phase !== 'playing')
|
|
1009
|
+
return;
|
|
1010
|
+
transition.phase = 'settling';
|
|
1011
|
+
// Ionic removes page transforms and applies hidden-page classes after onFinish.
|
|
1012
|
+
void painted().then(() => {
|
|
1013
|
+
if (stopped || navigation.current !== transition)
|
|
1014
|
+
return;
|
|
1015
|
+
transition.phase = 'committing';
|
|
1016
|
+
forceRefresh = true;
|
|
1017
|
+
schedule();
|
|
1018
|
+
});
|
|
1019
|
+
};
|
|
1020
|
+
const pageWill = (event) => {
|
|
1021
|
+
const page = event.target;
|
|
1022
|
+
// Popover content has Ionic page lifecycle too; it is not a page navigation.
|
|
1023
|
+
if (page.closest('ion-popover'))
|
|
1024
|
+
return;
|
|
1025
|
+
if (!page.closest('ion-modal'))
|
|
1026
|
+
imagesFor()?.invalidate();
|
|
1027
|
+
verticalBarsPages.lifecycle(event);
|
|
1028
|
+
tabActivationPages.delete(page);
|
|
1029
|
+
getNativeSearchBindings(doc)
|
|
1030
|
+
.filter((binding) => page.contains(binding.footer))
|
|
1031
|
+
.forEach((binding) => search.retire(binding));
|
|
1032
|
+
// Angular updates the URL before willLeave, but keeps ion-tab-bar.selectedTab on the
|
|
1033
|
+
// leaving tab until DidChange. A mismatch means this leave is a tab switch, not a stack push.
|
|
1034
|
+
if (event.type === LIFECYCLE_WILL_LEAVE)
|
|
1035
|
+
beginTabSwitchHandoffIfNeeded(page);
|
|
1036
|
+
if (event.type === LIFECYCLE_WILL_LEAVE && !tabSwitchHandoff && !manualSuspensions.size && !overlayOpen()) {
|
|
1037
|
+
navigation.begin([...pages, page], acknowledged);
|
|
1038
|
+
for (const entering of pages)
|
|
1039
|
+
if (entering !== page)
|
|
1040
|
+
maskNavigationPage(entering);
|
|
1041
|
+
}
|
|
1042
|
+
else if (event.type === LIFECYCLE_WILL_ENTER && navigation.current) {
|
|
1043
|
+
navigation.begin([page], acknowledged);
|
|
1044
|
+
maskNavigationPage(page);
|
|
1045
|
+
}
|
|
1046
|
+
pages.add(page);
|
|
1047
|
+
if (event.type === LIFECYCLE_WILL_ENTER && nativeVerticalBars())
|
|
1048
|
+
search.prepare(page);
|
|
1049
|
+
schedule();
|
|
1050
|
+
};
|
|
1051
|
+
const pageDid = (event) => {
|
|
1052
|
+
const page = event.target;
|
|
1053
|
+
if (page.closest('ion-popover'))
|
|
1054
|
+
return;
|
|
1055
|
+
verticalBarsPages.lifecycle(event);
|
|
1056
|
+
tabActivationPages.delete(page);
|
|
1057
|
+
pages.delete(page);
|
|
1058
|
+
if (navigation.current && !navigation.current.leased && pages.size === 0)
|
|
1059
|
+
finishNavigation();
|
|
1060
|
+
schedule();
|
|
1061
|
+
if (tabSwitchHandoff && pages.size === 0)
|
|
1062
|
+
endTabSwitchHandoff();
|
|
1063
|
+
};
|
|
1064
|
+
const tabOfPath = (tabs, pathname) => {
|
|
1065
|
+
let best;
|
|
1066
|
+
for (const button of Array.from(tabs.querySelectorAll('ion-tab-button'))) {
|
|
1067
|
+
const tab = button.getAttribute('tab');
|
|
1068
|
+
const href = button.getAttribute('href')?.split(/[?#]/)[0];
|
|
1069
|
+
if (!tab || !href)
|
|
1070
|
+
continue;
|
|
1071
|
+
if (pathname === href || pathname.startsWith(`${href}/`)) {
|
|
1072
|
+
if (!best || href.length > best.length)
|
|
1073
|
+
best = { tab, length: href.length };
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
return best?.tab;
|
|
1077
|
+
};
|
|
1078
|
+
const armTabSwitchHandoff = () => {
|
|
1079
|
+
imagesFor()?.invalidate();
|
|
1080
|
+
tabSwitchHandoff = true;
|
|
1081
|
+
navigation.clear();
|
|
1082
|
+
interruptAssets?.();
|
|
1083
|
+
crossfade.finish();
|
|
1084
|
+
// Retiring outgoing controls often lands in a later async sync than WillLeave/DidChange.
|
|
1085
|
+
handoffUntil = Math.max(handoffUntil, win.performance.now() + 400);
|
|
1086
|
+
// The in-flight sync already captured handoffInstant=false; hold instant for the next one.
|
|
1087
|
+
if (updateQueue.running)
|
|
1088
|
+
handoffAcrossPending = true;
|
|
1089
|
+
};
|
|
1090
|
+
const beginTabSwitchHandoffIfNeeded = (page) => {
|
|
1091
|
+
const tabs = page.closest('ion-tabs');
|
|
1092
|
+
if (!tabs)
|
|
1093
|
+
return;
|
|
1094
|
+
const bar = tabs.querySelector('ion-tab-bar');
|
|
1095
|
+
const selected = (bar && 'selectedTab' in bar ? String(bar.selectedTab ?? '') : '') ||
|
|
1096
|
+
tabs.querySelector('ion-tab-button.tab-selected')?.getAttribute('tab') ||
|
|
1097
|
+
undefined;
|
|
1098
|
+
const destination = tabOfPath(tabs, win.location.pathname);
|
|
1099
|
+
if (selected && destination && selected !== destination)
|
|
1100
|
+
armTabSwitchHandoff();
|
|
1101
|
+
};
|
|
1102
|
+
// DidChange / pageDid can land in the same turn as WillLeave. Clear the sticky flag on the
|
|
1103
|
+
// next frame; handoffUntil / handoffAcrossPending still cover the async retire sync.
|
|
1104
|
+
const endTabSwitchHandoff = () => {
|
|
1105
|
+
win.requestAnimationFrame(() => {
|
|
1106
|
+
if (stopped || !tabSwitchHandoff)
|
|
1107
|
+
return;
|
|
1108
|
+
tabSwitchHandoff = false;
|
|
1109
|
+
schedule();
|
|
1110
|
+
});
|
|
1111
|
+
};
|
|
1112
|
+
const followImageMotion = () => projectionSurfaces.followMotion(moving, (source) => projectionState.getTarget(source) ?? source);
|
|
1113
|
+
const motion = (event) => {
|
|
1114
|
+
const target = event.target;
|
|
1115
|
+
if (!target.matches?.(`${motionSelector}, ${discovery}`) && !(options.source !== 'ionic' && target.querySelector?.(discovery)))
|
|
1116
|
+
return;
|
|
1117
|
+
const key = event.propertyName ?? event.animationName;
|
|
1118
|
+
if (event.type === CSS_MOTION_EVENTS.transitionRun || event.type === CSS_MOTION_EVENTS.animationStart) {
|
|
1119
|
+
const keys = moving.get(target) ?? new Set();
|
|
1120
|
+
keys.add(key);
|
|
1121
|
+
moving.set(target, keys);
|
|
1122
|
+
}
|
|
1123
|
+
else {
|
|
1124
|
+
moving.get(target)?.delete(key);
|
|
1125
|
+
if (!moving.get(target)?.size)
|
|
1126
|
+
moving.delete(target);
|
|
1127
|
+
}
|
|
1128
|
+
followImageMotion();
|
|
1129
|
+
schedule();
|
|
1130
|
+
};
|
|
1131
|
+
for (const name of Object.values(CSS_MOTION_EVENTS))
|
|
1132
|
+
on(doc, name, motion);
|
|
1133
|
+
for (const name of [LIFECYCLE_WILL_ENTER, LIFECYCLE_WILL_LEAVE])
|
|
1134
|
+
on(doc, name, pageWill);
|
|
1135
|
+
for (const name of [LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE])
|
|
1136
|
+
on(doc, name, pageDid);
|
|
1137
|
+
on(doc, VERTICAL_BARS_TRANSITION_CANCELED, (event) => {
|
|
1138
|
+
const leaving = event.target;
|
|
1139
|
+
const entering = event.detail?.entering;
|
|
1140
|
+
verticalBarsPages.cancel(entering, leaving);
|
|
1141
|
+
pages.delete(leaving);
|
|
1142
|
+
if (entering)
|
|
1143
|
+
pages.delete(entering);
|
|
1144
|
+
if (!navigation.current?.leased)
|
|
1145
|
+
finishNavigation();
|
|
1146
|
+
schedule();
|
|
1147
|
+
});
|
|
1148
|
+
on(doc, 'ionTabsWillChange', () => {
|
|
1149
|
+
// Vanilla ion-tabs dispatches DOM events; @ionic/angular uses EventEmitters instead.
|
|
1150
|
+
armTabSwitchHandoff();
|
|
1151
|
+
schedule();
|
|
1152
|
+
});
|
|
1153
|
+
on(doc, 'ionTabsDidChange', endTabSwitchHandoff);
|
|
1154
|
+
const overlayPresenting = (element) => {
|
|
1155
|
+
if (overlayMonitor.isExcluded(element))
|
|
1156
|
+
return;
|
|
1157
|
+
imagesFor(coveredSurface(element))?.retireRestoration();
|
|
1158
|
+
if (element.matches('ion-popover')) {
|
|
1159
|
+
// Retention is chosen synchronously by the native overlay controller.
|
|
1160
|
+
// Otherwise a blocking Web popover must sit above the entire rail.
|
|
1161
|
+
if (usesCoveredBackground(element))
|
|
1162
|
+
blockingRailOverlays.add(element);
|
|
1163
|
+
else
|
|
1164
|
+
blockingRailOverlays.delete(element);
|
|
1165
|
+
}
|
|
1166
|
+
if (element.matches('ion-picker') && usesCoveredBackground(element))
|
|
1167
|
+
blockingRailOverlays.add(element);
|
|
1168
|
+
if (element.matches('ion-modal')) {
|
|
1169
|
+
const background = imagesFor(coveredSurface(element));
|
|
1170
|
+
if (background)
|
|
1171
|
+
coverOverlayBackground(element, background);
|
|
1172
|
+
modalHandoff = true;
|
|
1173
|
+
// A pending bridge update must retire the old controls immediately too.
|
|
1174
|
+
if (updateQueue.running)
|
|
1175
|
+
handoffInstant = true;
|
|
1176
|
+
}
|
|
1177
|
+
// Let the overlay controller choose Native before the optional Web handoff.
|
|
1178
|
+
if (!element.matches('ion-modal'))
|
|
1179
|
+
win.queueMicrotask(coverWebOverlay);
|
|
1180
|
+
schedule();
|
|
1181
|
+
};
|
|
1182
|
+
const onOverlay = (callback) => {
|
|
1183
|
+
const remove = overlayMonitor.subscribe(callback);
|
|
1184
|
+
listeners.signal.addEventListener('abort', remove, { once: true });
|
|
1185
|
+
};
|
|
1186
|
+
onOverlay(({ element, phase, event }) => {
|
|
1187
|
+
if (phase === 'change')
|
|
1188
|
+
projectionSurfaces.end();
|
|
1189
|
+
if (!element) {
|
|
1190
|
+
schedule();
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
if (element.matches('ion-menu')) {
|
|
1194
|
+
if (phase === 'willPresent')
|
|
1195
|
+
imagesFor()?.retireRestoration();
|
|
1196
|
+
schedule();
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
if (phase === 'willPresent')
|
|
1200
|
+
overlayPresenting(element);
|
|
1201
|
+
if (event?.type === 'ionBreakpointDidChange') {
|
|
1202
|
+
const modal = element;
|
|
1203
|
+
const breakpoint = event.detail?.breakpoint;
|
|
1204
|
+
if (breakpoint !== undefined && breakpoint > 0 && breakpoint <= (modal.backdropBreakpoint ?? 0))
|
|
1205
|
+
imagesFor(coveredSurface(modal))?.invalidate();
|
|
1206
|
+
}
|
|
1207
|
+
if (phase === 'willDismiss' && element.matches('ion-modal')) {
|
|
1208
|
+
projectionSurfaces.get(element)?.begin();
|
|
1209
|
+
modalHandoff = true;
|
|
1210
|
+
if (updateQueue.running)
|
|
1211
|
+
handoffInstant = true;
|
|
1212
|
+
schedule();
|
|
1213
|
+
}
|
|
1214
|
+
if (phase === 'didDismiss') {
|
|
1215
|
+
blockingRailOverlays.delete(element);
|
|
1216
|
+
if (element.matches('ion-modal')) {
|
|
1217
|
+
modalHandoff = true;
|
|
1218
|
+
if (updateQueue.running)
|
|
1219
|
+
handoffInstant = true;
|
|
1220
|
+
const modal = element;
|
|
1221
|
+
void projectionSurfaces.delete(modal);
|
|
1222
|
+
}
|
|
1223
|
+
projectionSurfaces.end();
|
|
1224
|
+
schedule();
|
|
1225
|
+
}
|
|
1226
|
+
});
|
|
1227
|
+
for (const name of [
|
|
1228
|
+
'ionChange',
|
|
1229
|
+
'ionSelect',
|
|
1230
|
+
'ionImgDidLoad',
|
|
1231
|
+
CSS_MOTION_EVENTS.transitionEnd,
|
|
1232
|
+
CSS_MOTION_EVENTS.animationEnd,
|
|
1233
|
+
'focusin',
|
|
1234
|
+
'focusout',
|
|
1235
|
+
])
|
|
1236
|
+
on(doc, name);
|
|
1237
|
+
const refreshLayout = () => {
|
|
1238
|
+
rejected = new WeakMap();
|
|
1239
|
+
forceRefresh = true;
|
|
1240
|
+
schedule();
|
|
1241
|
+
};
|
|
1242
|
+
on(doc, 'visibilitychange', () => {
|
|
1243
|
+
if (doc.hidden) {
|
|
1244
|
+
getNativeSearchBindings(doc).forEach((binding) => search.retire(binding));
|
|
1245
|
+
schedule();
|
|
1246
|
+
}
|
|
1247
|
+
else
|
|
1248
|
+
refreshLayout();
|
|
1249
|
+
});
|
|
1250
|
+
on(win, 'resize', () => {
|
|
1251
|
+
navigation.clear();
|
|
1252
|
+
refreshLayout();
|
|
1253
|
+
});
|
|
1254
|
+
on(win, 'keyboardWillShow', () => search.keyboard(true));
|
|
1255
|
+
on(win, 'keyboardWillHide', () => search.keyboard(false));
|
|
1256
|
+
on(win, 'scroll', scrollChanged);
|
|
1257
|
+
on(win, 'scrollend', scrollSettled);
|
|
1258
|
+
on(win.matchMedia('(prefers-color-scheme: dark)'), 'change');
|
|
1259
|
+
on(win, 'nativeUIRefresh', (event) => {
|
|
1260
|
+
if (imagesFor() && event.detail?.modalMotion)
|
|
1261
|
+
return;
|
|
1262
|
+
if (nativeVerticalBars())
|
|
1263
|
+
search.prepare();
|
|
1264
|
+
if (event.retireSearch)
|
|
1265
|
+
getNativeSearchBindings(doc).forEach((binding) => search.retire(binding));
|
|
1266
|
+
refreshLayout();
|
|
1267
|
+
});
|
|
1268
|
+
if (win.visualViewport) {
|
|
1269
|
+
on(win.visualViewport, 'resize', refreshLayout);
|
|
1270
|
+
on(win.visualViewport, 'scroll');
|
|
1271
|
+
}
|
|
1272
|
+
const handle = {
|
|
1273
|
+
// A native overlay covers this surface; keep its controls and Web ownership intact.
|
|
1274
|
+
retain(deferActivation) {
|
|
1275
|
+
const retention = { accepts: deferActivation };
|
|
1276
|
+
retentions.add(retention);
|
|
1277
|
+
return () => {
|
|
1278
|
+
if (!retentions.delete(retention))
|
|
1279
|
+
return;
|
|
1280
|
+
// UIKit may already expose the anchor while Ionic is still dismissing.
|
|
1281
|
+
// Replay at most one validated input after the live content is restored.
|
|
1282
|
+
const activation = retention.activation;
|
|
1283
|
+
if (activation && retention.accepts?.(activation.id))
|
|
1284
|
+
win.queueMicrotask(() => activate(activation));
|
|
1285
|
+
schedule();
|
|
1286
|
+
};
|
|
1287
|
+
},
|
|
1288
|
+
markForCheck: schedule,
|
|
1289
|
+
getStatus: () => ({
|
|
1290
|
+
state: stopped ? 'stopped' : sources.size ? 'native' : 'web',
|
|
1291
|
+
projected: sources.size,
|
|
1292
|
+
updates,
|
|
1293
|
+
reason,
|
|
1294
|
+
}),
|
|
1295
|
+
async suspend(suspensionOptions) {
|
|
1296
|
+
const token = Symbol();
|
|
1297
|
+
let resumed = false;
|
|
1298
|
+
if (!stopped) {
|
|
1299
|
+
navigation.clear();
|
|
1300
|
+
verticalGesture = undefined;
|
|
1301
|
+
manualSuspensions.set(token, suspensionOptions?.covered === true);
|
|
1302
|
+
if ([...manualSuspensions.values()].every(Boolean))
|
|
1303
|
+
imagesFor(topModal(doc))?.begin();
|
|
1304
|
+
else
|
|
1305
|
+
projectionSurfaces.values().forEach((surface) => {
|
|
1306
|
+
if (surface.token || surface.restoring)
|
|
1307
|
+
surface.invalidate();
|
|
1308
|
+
});
|
|
1309
|
+
getNativeSearchBindings(doc).forEach((binding) => search.retire(binding));
|
|
1310
|
+
await flush();
|
|
1311
|
+
}
|
|
1312
|
+
return {
|
|
1313
|
+
async resume() {
|
|
1314
|
+
if (resumed)
|
|
1315
|
+
return;
|
|
1316
|
+
resumed = true;
|
|
1317
|
+
if (stopped || !manualSuspensions.delete(token))
|
|
1318
|
+
return;
|
|
1319
|
+
projectionSurfaces.end();
|
|
1320
|
+
await flush();
|
|
1321
|
+
},
|
|
1322
|
+
};
|
|
1323
|
+
},
|
|
1324
|
+
destroy() {
|
|
1325
|
+
if (destruction)
|
|
1326
|
+
return destruction;
|
|
1327
|
+
stopped = true;
|
|
1328
|
+
stopConnectionCheck?.();
|
|
1329
|
+
destruction = Promise.resolve().then(async () => {
|
|
1330
|
+
tabGeometry.forEach((restore) => restore());
|
|
1331
|
+
tabGeometry.clear();
|
|
1332
|
+
tabWidths.clear();
|
|
1333
|
+
setNativeUIIntegration(doc);
|
|
1334
|
+
updateQueue.destroy();
|
|
1335
|
+
listeners.abort();
|
|
1336
|
+
observer.disconnect();
|
|
1337
|
+
resize.disconnect();
|
|
1338
|
+
observed.clear();
|
|
1339
|
+
actions.clear();
|
|
1340
|
+
pendingActivations = [];
|
|
1341
|
+
navigation.clear();
|
|
1342
|
+
acknowledged = [];
|
|
1343
|
+
scrollBindings.acknowledge([], 0);
|
|
1344
|
+
verticalGesture = undefined;
|
|
1345
|
+
search.destroy();
|
|
1346
|
+
crossfade.destroy();
|
|
1347
|
+
await projectionSurfaces.destroy();
|
|
1348
|
+
restoreAll();
|
|
1349
|
+
unregisterNativeOwner();
|
|
1350
|
+
if (!doc.hidden)
|
|
1351
|
+
await painted();
|
|
1352
|
+
try {
|
|
1353
|
+
await bounded(plugin.clear({ revision: ++revision }));
|
|
1354
|
+
}
|
|
1355
|
+
catch {
|
|
1356
|
+
/* Always restore the Web, even after bridge loss. */
|
|
1357
|
+
}
|
|
1358
|
+
style.remove();
|
|
1359
|
+
doc.querySelectorAll(`.${rejectedClass}`).forEach((element) => setRejected(element, false));
|
|
1360
|
+
icons.clear();
|
|
1361
|
+
pages.clear();
|
|
1362
|
+
manualSuspensions.clear();
|
|
1363
|
+
moving.clear();
|
|
1364
|
+
suspended.clear();
|
|
1365
|
+
if (listener)
|
|
1366
|
+
await bounded(listener.remove()).catch(() => { });
|
|
1367
|
+
if (searchListener)
|
|
1368
|
+
await bounded(searchListener.remove()).catch(() => { });
|
|
1369
|
+
finishWaiters();
|
|
1370
|
+
});
|
|
1371
|
+
return destruction;
|
|
1372
|
+
},
|
|
1373
|
+
};
|
|
1374
|
+
const activate = (event) => {
|
|
1375
|
+
const accepted = dispatchActivation(event);
|
|
1376
|
+
if (event.requestId && accepted !== undefined) {
|
|
1377
|
+
void bounded(plugin.completeExtensionAction({ requestId: event.requestId, accepted })).catch(() => { });
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
const dispatchActivation = (event) => {
|
|
1381
|
+
if (stopped || doc.hidden || event.revision < acceptedRevision || event.revision > revision || event.sequence <= lastSequence)
|
|
1382
|
+
return false;
|
|
1383
|
+
const element = actions.get(event.id);
|
|
1384
|
+
const owner = element && Array.from(sources.keys()).find((source) => source === element || source.contains(element));
|
|
1385
|
+
const ignored = owner && isVerticalBarsSource(owner) ? (retentions.size ? 'ion-toast, ion-picker' : railPreservingOverlays) : undefined;
|
|
1386
|
+
if (overlayOpen(false, true, ignored)) {
|
|
1387
|
+
if (!manualSuspensions.size) {
|
|
1388
|
+
for (const retention of retentions) {
|
|
1389
|
+
if (retention.accepts?.(event.id) && (!retention.activation || event.sequence > retention.activation.sequence))
|
|
1390
|
+
retention.activation = event;
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
return false;
|
|
1394
|
+
}
|
|
1395
|
+
// Native may send input before update() resolves on the JS bridge.
|
|
1396
|
+
// Revalidate it after ownership is accepted, using the same action path.
|
|
1397
|
+
if (event.revision > acceptedRevision && event.revision === updatingRevision) {
|
|
1398
|
+
pendingActivations.push(event);
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
// A failed update may have reached native without acknowledging its actions.
|
|
1402
|
+
if (event.revision > acceptedRevision)
|
|
1403
|
+
return false;
|
|
1404
|
+
lastSequence = event.sequence;
|
|
1405
|
+
if (!element || (verticalGesture && owner && isVerticalBarsSource(owner)))
|
|
1406
|
+
return false;
|
|
1407
|
+
if (!owner || !inVerticalBarsSurface(owner))
|
|
1408
|
+
return false;
|
|
1409
|
+
const direct = !blocked(owner) && unprojected(sources.keys(), () => readEnabledCandidate(owner));
|
|
1410
|
+
const candidate = direct || read().find((candidate) => candidate.actions.has(event.id));
|
|
1411
|
+
// Only retained horizontal controls are locked during navigation. Vertical
|
|
1412
|
+
// Bars and other toolbar controls keep their existing activation lifecycle.
|
|
1413
|
+
if (candidate && horizontalToolbar(candidate) && navigation.owns(element))
|
|
1414
|
+
return false;
|
|
1415
|
+
const item = candidate?.control.items.find((item) => item.id === event.id);
|
|
1416
|
+
const searchAction = candidate?.control.search && [candidate.control.search.trigger.id, candidate.control.search.closeId].includes(event.id);
|
|
1417
|
+
const extensionAction = candidate?.control.kind === 'extension' && candidate.actions.get(event.id) === element;
|
|
1418
|
+
if (!extensionAction && !searchAction && (!item || item.disabled || item.visible === false))
|
|
1419
|
+
return false;
|
|
1420
|
+
// A native tab tap must release the old page's covers before Ionic exposes
|
|
1421
|
+
// the destination. Waiting for WillLeave is too late for an unanimated swap.
|
|
1422
|
+
if (element.matches('ion-tab-button'))
|
|
1423
|
+
lastTabSequence = event.sequence;
|
|
1424
|
+
if (element.matches('ion-tab-button') && !element.classList.contains('tab-selected')) {
|
|
1425
|
+
const tabs = element.closest('ion-tabs');
|
|
1426
|
+
const leaving = new Set(acknowledged
|
|
1427
|
+
.map((source) => source.element.closest('.ion-page'))
|
|
1428
|
+
.filter((page) => !!page && !!tabs?.contains(page)));
|
|
1429
|
+
leaving.forEach((page) => tabActivationPages.set(page, event.sequence));
|
|
1430
|
+
armTabSwitchHandoff();
|
|
1431
|
+
void flush().then(() => {
|
|
1432
|
+
if (stopped)
|
|
1433
|
+
return;
|
|
1434
|
+
if (event.sequence === lastTabSequence && element.isConnected && !element.disabled)
|
|
1435
|
+
activateProjectedElement(element);
|
|
1436
|
+
// WillLeave/WillEnter release these scopes once Ionic commits the route.
|
|
1437
|
+
// If a router guard rejects it, restore native ownership after the handoff window.
|
|
1438
|
+
win.setTimeout(() => {
|
|
1439
|
+
leaving.forEach((page) => {
|
|
1440
|
+
if (tabActivationPages.get(page) === event.sequence)
|
|
1441
|
+
tabActivationPages.delete(page);
|
|
1442
|
+
});
|
|
1443
|
+
// Rejected selection emits no DidChange/pageDid to end the handoff.
|
|
1444
|
+
if (!tabActivationPages.size && !pages.size)
|
|
1445
|
+
endTabSwitchHandoff();
|
|
1446
|
+
if (!stopped)
|
|
1447
|
+
schedule();
|
|
1448
|
+
}, Math.max(0, handoffUntil - win.performance.now()));
|
|
1449
|
+
lastSnapshot = '';
|
|
1450
|
+
schedule();
|
|
1451
|
+
});
|
|
1452
|
+
return true;
|
|
1453
|
+
}
|
|
1454
|
+
// The original Ionic host owns form submission, routerLink and selection events.
|
|
1455
|
+
activateProjectedElement(element);
|
|
1456
|
+
lastSnapshot = ''; // Reconcile even if Ionic rejects the proposed native selection.
|
|
1457
|
+
schedule();
|
|
1458
|
+
return true;
|
|
1459
|
+
};
|
|
1460
|
+
if (options.coveredSnapshots && plugin.coveredSnapshot) {
|
|
1461
|
+
try {
|
|
1462
|
+
const shared = {
|
|
1463
|
+
wake: () => {
|
|
1464
|
+
schedule();
|
|
1465
|
+
win.dispatchEvent(new Event(sourcesChangedEvent));
|
|
1466
|
+
},
|
|
1467
|
+
revision: () => revision,
|
|
1468
|
+
};
|
|
1469
|
+
await projectionSurfaces.ensure(doc, () => createCoveredSnapshots(doc, plugin, connection?.listen ?? plugin.addListener.bind(plugin), {
|
|
1470
|
+
...shared,
|
|
1471
|
+
blocked: () => overlayOpen() || !!navigation.current || !!verticalGesture || stopped,
|
|
1472
|
+
}));
|
|
1473
|
+
const prepareSurface = async (modal) => {
|
|
1474
|
+
if (projectionSurfaces.get(modal))
|
|
1475
|
+
return;
|
|
1476
|
+
try {
|
|
1477
|
+
const surface = await projectionSurfaces.ensure(modal, () => createCoveredSnapshots(doc, plugin, connection?.listen ?? plugin.addListener.bind(plugin), {
|
|
1478
|
+
...shared,
|
|
1479
|
+
// The source surface is immutable; foreground ownership may change.
|
|
1480
|
+
modal,
|
|
1481
|
+
blocked: () => topModal(doc) !== modal ||
|
|
1482
|
+
modalPreparing(modal) ||
|
|
1483
|
+
!!modalPresentation(modal) ||
|
|
1484
|
+
movingModal(doc) === modal ||
|
|
1485
|
+
!!navigation.current ||
|
|
1486
|
+
stopped ||
|
|
1487
|
+
overlayOpen(true, true),
|
|
1488
|
+
}));
|
|
1489
|
+
if (surface && !modalStack(doc).includes(modal))
|
|
1490
|
+
await projectionSurfaces.delete(modal);
|
|
1491
|
+
else if (surface)
|
|
1492
|
+
schedule();
|
|
1493
|
+
}
|
|
1494
|
+
catch {
|
|
1495
|
+
// This surface keeps the existing Web fallback if the optional listener fails.
|
|
1496
|
+
}
|
|
1497
|
+
};
|
|
1498
|
+
onOverlay(({ element, phase }) => {
|
|
1499
|
+
if (element?.matches('ion-modal') && (phase === 'willPresent' || phase === 'didPresent'))
|
|
1500
|
+
void prepareSurface(element);
|
|
1501
|
+
});
|
|
1502
|
+
await Promise.all(modalStack(doc).map(prepareSurface));
|
|
1503
|
+
on(win, modalMotionEvent, (event) => {
|
|
1504
|
+
const { modal, moving } = event.detail;
|
|
1505
|
+
const surface = projectionSurfaces.get(modal);
|
|
1506
|
+
if (moving)
|
|
1507
|
+
surface?.begin();
|
|
1508
|
+
else if (!modalDismissing(modal)) {
|
|
1509
|
+
lastSnapshot = '';
|
|
1510
|
+
surface?.end();
|
|
1511
|
+
}
|
|
1512
|
+
schedule();
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
catch {
|
|
1516
|
+
/* Optional capability: older/mismatched Web bundles keep the existing fallback. */
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
try {
|
|
1520
|
+
const listen = connection?.listen ?? plugin.addListener.bind(plugin);
|
|
1521
|
+
listener = await boundedListener(listen('activate', activate));
|
|
1522
|
+
searchListener = await boundedListener(listen('search', (event) => {
|
|
1523
|
+
if (stopped ||
|
|
1524
|
+
doc.hidden ||
|
|
1525
|
+
event.revision < acceptedRevision ||
|
|
1526
|
+
event.revision > revision ||
|
|
1527
|
+
overlayOpen(true, false, nativeVerticalBars() ? (retentions.size ? 'ion-toast, ion-picker' : railPreservingOverlays) : undefined))
|
|
1528
|
+
return;
|
|
1529
|
+
search.event(event);
|
|
1530
|
+
}));
|
|
1531
|
+
stopConnectionCheck = connection?.observe((state, restored, requestedAt) => {
|
|
1532
|
+
if (stopped || updateQueue.running || lastUpdateStarted > requestedAt)
|
|
1533
|
+
return;
|
|
1534
|
+
const restarted = (connectionSession !== undefined && connectionSession !== state.session) ||
|
|
1535
|
+
(state.revision < acceptedRevision && state.sequence < lastSequence);
|
|
1536
|
+
connectionSession = state.session;
|
|
1537
|
+
if (restarted || restored) {
|
|
1538
|
+
projectionSurfaces.invalidate();
|
|
1539
|
+
}
|
|
1540
|
+
const missingSurface = acknowledged.some(({ control }) => !control.extension?.tabBarId && !(control.placement === 'vertical-bars' ? state.surfaces.vertical : state.surfaces.horizontal));
|
|
1541
|
+
if (!restored && !updateQueue.waiting && !missingSurface && state.revision === acceptedRevision)
|
|
1542
|
+
return;
|
|
1543
|
+
// Native may have restarted or advanced while an acknowledgement was lost.
|
|
1544
|
+
// Advance the revision gate before refreshing, rejecting stale action events.
|
|
1545
|
+
revision = Math.max(revision, state.revision);
|
|
1546
|
+
acceptedRevision = revision + 1;
|
|
1547
|
+
lastSequence = restarted ? state.sequence : Math.max(lastSequence, state.sequence);
|
|
1548
|
+
lastTabSequence = 0;
|
|
1549
|
+
pendingActivations = [];
|
|
1550
|
+
updateQueue.reconnected();
|
|
1551
|
+
forceRefresh = true;
|
|
1552
|
+
lastSnapshot = '';
|
|
1553
|
+
schedule();
|
|
1554
|
+
}, async (error) => {
|
|
1555
|
+
if (!stopped)
|
|
1556
|
+
await fail(error);
|
|
1557
|
+
});
|
|
1558
|
+
doc.head.append(style);
|
|
1559
|
+
observer.observe(doc.documentElement, observation);
|
|
1560
|
+
setNativeUIIntegration(doc, {
|
|
1561
|
+
async search(binding, active, focus) {
|
|
1562
|
+
if (!getNativeSearchBindings(doc).includes(binding) || !search.projected(binding))
|
|
1563
|
+
return false;
|
|
1564
|
+
if (!active) {
|
|
1565
|
+
search.setStatus(binding, false);
|
|
1566
|
+
}
|
|
1567
|
+
else {
|
|
1568
|
+
const entering = !binding.active;
|
|
1569
|
+
if (entering)
|
|
1570
|
+
search.begin(binding, revision + 1);
|
|
1571
|
+
const nextFocused = focus === true ? true : focus === false ? false : entering ? false : binding.focused;
|
|
1572
|
+
search.setStatus(binding, true, nextFocused);
|
|
1573
|
+
}
|
|
1574
|
+
await flush();
|
|
1575
|
+
return !stopped && search.projected(binding);
|
|
1576
|
+
},
|
|
1577
|
+
async suspend(scopes, suspensionOptions) {
|
|
1578
|
+
verticalGesture = undefined;
|
|
1579
|
+
if (!suspensionOptions?.navigation) {
|
|
1580
|
+
navigation.clear();
|
|
1581
|
+
}
|
|
1582
|
+
let transition;
|
|
1583
|
+
let finishVertical;
|
|
1584
|
+
if (suspensionOptions?.navigation &&
|
|
1585
|
+
!tabSwitchHandoff &&
|
|
1586
|
+
!overlayOpen(true, true) &&
|
|
1587
|
+
scopes[0]?.closest('ion-app.ios-theme-vertical-bars') &&
|
|
1588
|
+
scopes.every(inVerticalBarsSurface)) {
|
|
1589
|
+
finishVertical = verticalNavigation.begin(scopes[0], scopes[1], suspensionOptions.direction);
|
|
1590
|
+
if (suspensionOptions.interactive) {
|
|
1591
|
+
verticalGesture = {
|
|
1592
|
+
pending: true,
|
|
1593
|
+
scopes,
|
|
1594
|
+
sources: new Set(acknowledged.filter((candidate) => candidate.control.placement === 'vertical-bars').flatMap(candidateSources)),
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
if (suspensionOptions?.navigation && !manualSuspensions.size && !overlayOpen() && !tabSwitchHandoff) {
|
|
1599
|
+
transition = navigation.begin(scopes, acknowledged, true);
|
|
1600
|
+
maskNavigationPage(scopes[0]);
|
|
1601
|
+
// Keep outgoing surfaces native through playback, including those with
|
|
1602
|
+
// no destination. Native progress fades them out in place.
|
|
1603
|
+
try {
|
|
1604
|
+
await Promise.all(transition.destination.flatMap((candidate) => candidate.icons.map(async (icon) => {
|
|
1605
|
+
icon.item.icon = await icons.render(icon.source, icon.item.iconWidth, icon.item.iconHeight);
|
|
1606
|
+
})));
|
|
1607
|
+
}
|
|
1608
|
+
catch (error) {
|
|
1609
|
+
await fail(error);
|
|
1610
|
+
return () => { };
|
|
1611
|
+
}
|
|
1612
|
+
transition.prepared = transition.frozen.length > 0 || transition.destination.length > 0;
|
|
1613
|
+
}
|
|
1614
|
+
const gesture = verticalGesture;
|
|
1615
|
+
getNativeSearchBindings(doc)
|
|
1616
|
+
.filter((binding) => scopes.some((scope) => scope.contains(binding.footer) || scope.contains(binding.tabBar)))
|
|
1617
|
+
.forEach((binding) => search.retire(binding));
|
|
1618
|
+
suspended.add(scopes);
|
|
1619
|
+
await flush(transition);
|
|
1620
|
+
// Moving content has returned to Web; fixed toolbar covers remain native for navigation.
|
|
1621
|
+
await new Promise((resolve) => win.requestAnimationFrame(() => resolve()));
|
|
1622
|
+
const release = (canceled = false) => {
|
|
1623
|
+
const currentVertical = finishVertical?.(canceled) !== false;
|
|
1624
|
+
suspended.delete(scopes);
|
|
1625
|
+
if (!currentVertical)
|
|
1626
|
+
return;
|
|
1627
|
+
if (verticalGesture === gesture)
|
|
1628
|
+
verticalGesture = undefined;
|
|
1629
|
+
if (canceled || !scopes.some((scope) => scope.closest('ion-app.ios-theme-vertical-bars'))) {
|
|
1630
|
+
scopes.forEach((scope) => pages.delete(scope)); // Preserve ordinary iPhone handoff; cancellation has no DidLeave.
|
|
1631
|
+
if (canceled) {
|
|
1632
|
+
verticalBarsPages.cancel(scopes[0], scopes[1]); // The entering page is abandoned; the leaving page stays active.
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
if (transition)
|
|
1636
|
+
finishNavigation(transition);
|
|
1637
|
+
schedule();
|
|
1638
|
+
};
|
|
1639
|
+
if (gesture) {
|
|
1640
|
+
release.finishGesture = (complete) => {
|
|
1641
|
+
if (!complete || verticalGesture !== gesture)
|
|
1642
|
+
return;
|
|
1643
|
+
gesture.pending = false;
|
|
1644
|
+
schedule();
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
if (transition?.native) {
|
|
1648
|
+
let requested = 0;
|
|
1649
|
+
let sent = -1;
|
|
1650
|
+
let sending = false;
|
|
1651
|
+
release.progress = (value) => {
|
|
1652
|
+
requested = Math.max(0, Math.min(1, value));
|
|
1653
|
+
if (sending || requested === sent || navigation.current !== transition)
|
|
1654
|
+
return;
|
|
1655
|
+
sending = true;
|
|
1656
|
+
void (async () => {
|
|
1657
|
+
try {
|
|
1658
|
+
do {
|
|
1659
|
+
sent = requested;
|
|
1660
|
+
await bounded(plugin.setToolbarTransitionProgress({ id: transition.id, progress: sent }));
|
|
1661
|
+
} while (requested !== sent && navigation.current === transition);
|
|
1662
|
+
}
|
|
1663
|
+
catch (error) {
|
|
1664
|
+
await fail(error);
|
|
1665
|
+
}
|
|
1666
|
+
finally {
|
|
1667
|
+
sending = false;
|
|
1668
|
+
}
|
|
1669
|
+
})();
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
return release;
|
|
1673
|
+
},
|
|
1674
|
+
});
|
|
1675
|
+
on(win, 'pagehide', () => {
|
|
1676
|
+
void handle.destroy();
|
|
1677
|
+
onStop?.('pagehide');
|
|
1678
|
+
});
|
|
1679
|
+
schedule();
|
|
1680
|
+
}
|
|
1681
|
+
catch (error) {
|
|
1682
|
+
await fail(error);
|
|
1683
|
+
}
|
|
1684
|
+
return handle;
|
|
1685
|
+
};
|