@ionic/react-router 8.8.6-dev.11778015413.1dc85fcb → 8.8.6-dev.11778097327.1321e918
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/dist/index.js +671 -3422
- package/dist/index.js.map +1 -1
- package/dist/types/ReactRouter/IonReactHashRouter.d.ts +22 -7
- package/dist/types/ReactRouter/IonReactMemoryRouter.d.ts +21 -7
- package/dist/types/ReactRouter/IonReactRouter.d.ts +21 -8
- package/dist/types/ReactRouter/IonRouteInner.d.ts +3 -1
- package/dist/types/ReactRouter/IonRouter.d.ts +38 -18
- package/dist/types/ReactRouter/ReactRouterViewStack.d.ts +6 -74
- package/dist/types/ReactRouter/StackManager.d.ts +30 -0
- package/dist/types/ReactRouter/utils/matchPath.d.ts +21 -0
- package/package.json +8 -7
- package/dist/types/ReactRouter/utils/computeParentPath.d.ts +0 -61
- package/dist/types/ReactRouter/utils/pathMatching.d.ts +0 -31
- package/dist/types/ReactRouter/utils/pathNormalization.d.ts +0 -22
- package/dist/types/ReactRouter/utils/routeElements.d.ts +0 -23
- package/dist/types/ReactRouter/utils/viewItemUtils.d.ts +0 -30
package/dist/index.js
CHANGED
|
@@ -1,2215 +1,245 @@
|
|
|
1
1
|
import { __rest } from 'tslib';
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { createBrowserHistory, createHashHistory } from 'history';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { withRouter, Router } from 'react-router-dom';
|
|
5
|
+
import { ViewStacks, generateId, IonRoute, ViewLifeCycleManager, StackContext, RouteManagerContext, getConfig, LocationHistory, NavManager } from '@ionic/react';
|
|
6
|
+
import { Route, matchPath as matchPath$1, Router as Router$1 } from 'react-router';
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
* The matchPath function is used only for matching paths, not rendering components or elements.
|
|
13
|
-
* @see https://reactrouter.com/v6/utils/match-path
|
|
14
|
-
*/
|
|
15
|
-
const matchPath = ({ pathname, componentProps }) => {
|
|
16
|
-
var _a, _b;
|
|
17
|
-
const { path, index } = componentProps, restProps = __rest(componentProps, ["path", "index"]);
|
|
18
|
-
// Handle index routes - they match when pathname is empty or just "/"
|
|
19
|
-
if (index && !path) {
|
|
20
|
-
if (pathname === '' || pathname === '/') {
|
|
21
|
-
return {
|
|
22
|
-
params: {},
|
|
23
|
-
pathname: pathname,
|
|
24
|
-
pathnameBase: pathname || '/',
|
|
25
|
-
pattern: {
|
|
26
|
-
path: '',
|
|
27
|
-
caseSensitive: false,
|
|
28
|
-
end: true,
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
// Handle empty path routes - they match when pathname is also empty or just "/"
|
|
35
|
-
if (path === '' || path === undefined) {
|
|
36
|
-
if (pathname === '' || pathname === '/') {
|
|
37
|
-
return {
|
|
38
|
-
params: {},
|
|
39
|
-
pathname: pathname,
|
|
40
|
-
pathnameBase: pathname || '/',
|
|
41
|
-
pattern: {
|
|
42
|
-
path: '',
|
|
43
|
-
caseSensitive: (_a = restProps.caseSensitive) !== null && _a !== void 0 ? _a : false,
|
|
44
|
-
end: (_b = restProps.end) !== null && _b !== void 0 ? _b : true,
|
|
45
|
-
},
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
// For relative paths (don't start with '/'), normalize both path and pathname for matching
|
|
51
|
-
if (!path.startsWith('/')) {
|
|
52
|
-
const matchOptions = Object.assign({ path: `/${path}` }, restProps);
|
|
53
|
-
if ((matchOptions === null || matchOptions === void 0 ? void 0 : matchOptions.end) === undefined) {
|
|
54
|
-
matchOptions.end = !path.endsWith('*');
|
|
55
|
-
}
|
|
56
|
-
const normalizedPathname = pathname.startsWith('/') ? pathname : `/${pathname}`;
|
|
57
|
-
const match = matchPath$1(matchOptions, normalizedPathname);
|
|
58
|
-
if (match) {
|
|
59
|
-
// Adjust the match to remove the leading '/' we added
|
|
60
|
-
return Object.assign(Object.assign({}, match), { pathname: pathname, pathnameBase: match.pathnameBase === '/' ? '/' : match.pathnameBase.slice(1), pattern: Object.assign(Object.assign({}, match.pattern), { path: path }) });
|
|
61
|
-
}
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
// For absolute paths, use React Router's matcher directly.
|
|
65
|
-
// React Router v6 routes default to `end: true` unless the pattern
|
|
66
|
-
// explicitly opts into wildcards with `*`. Mirror that behaviour so
|
|
67
|
-
// matching parity stays aligned with <Route>.
|
|
68
|
-
const matchOptions = Object.assign({ path }, restProps);
|
|
69
|
-
if ((matchOptions === null || matchOptions === void 0 ? void 0 : matchOptions.end) === undefined) {
|
|
70
|
-
matchOptions.end = !path.endsWith('*');
|
|
71
|
-
}
|
|
72
|
-
return matchPath$1(matchOptions, pathname);
|
|
73
|
-
};
|
|
74
|
-
/**
|
|
75
|
-
* Determines the portion of a pathname that a given route pattern should match against.
|
|
76
|
-
* For absolute route patterns we return the full pathname. For relative patterns we
|
|
77
|
-
* strip off the already-matched parent segments so React Router receives the remainder.
|
|
78
|
-
*/
|
|
79
|
-
const derivePathnameToMatch = (fullPathname, routePath) => {
|
|
80
|
-
var _a;
|
|
81
|
-
// For absolute or empty routes, use the full pathname as-is
|
|
82
|
-
if (!routePath || routePath === '' || routePath.startsWith('/')) {
|
|
83
|
-
return fullPathname;
|
|
84
|
-
}
|
|
85
|
-
const trimmedPath = fullPathname.startsWith('/') ? fullPathname.slice(1) : fullPathname;
|
|
86
|
-
if (!trimmedPath) {
|
|
87
|
-
// For root-level relative routes (pathname is "/" and routePath is relative),
|
|
88
|
-
// return the full pathname so matchPath can normalize both.
|
|
89
|
-
// This allows routes like <Route path="foo/*" .../> at root level to work correctly.
|
|
90
|
-
return fullPathname;
|
|
91
|
-
}
|
|
92
|
-
const fullSegments = trimmedPath.split('/').filter(Boolean);
|
|
93
|
-
if (fullSegments.length === 0) {
|
|
94
|
-
return '';
|
|
95
|
-
}
|
|
96
|
-
const routeSegments = routePath.split('/').filter(Boolean);
|
|
97
|
-
if (routeSegments.length === 0) {
|
|
98
|
-
return trimmedPath;
|
|
99
|
-
}
|
|
100
|
-
const wildcardIndex = routeSegments.findIndex((segment) => segment === '*' || segment === '**');
|
|
101
|
-
if (wildcardIndex >= 0) {
|
|
102
|
-
const baseSegments = routeSegments.slice(0, wildcardIndex);
|
|
103
|
-
if (baseSegments.length === 0) {
|
|
104
|
-
return trimmedPath;
|
|
105
|
-
}
|
|
106
|
-
const startIndex = fullSegments.findIndex((_, idx) => baseSegments.every((seg, segIdx) => {
|
|
107
|
-
const target = fullSegments[idx + segIdx];
|
|
108
|
-
if (!target) {
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
111
|
-
if (seg.startsWith(':')) {
|
|
112
|
-
return true;
|
|
113
|
-
}
|
|
114
|
-
return target === seg;
|
|
115
|
-
}));
|
|
116
|
-
if (startIndex >= 0) {
|
|
117
|
-
return fullSegments.slice(startIndex).join('/');
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
if (routeSegments.length <= fullSegments.length) {
|
|
121
|
-
return fullSegments.slice(fullSegments.length - routeSegments.length).join('/');
|
|
122
|
-
}
|
|
123
|
-
return (_a = fullSegments[fullSegments.length - 1]) !== null && _a !== void 0 ? _a : trimmedPath;
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Finds the longest common prefix among an array of paths.
|
|
128
|
-
* Used to determine the scope of an outlet with absolute routes.
|
|
129
|
-
*
|
|
130
|
-
* @param paths An array of absolute path strings.
|
|
131
|
-
* @returns The common prefix shared by all paths.
|
|
132
|
-
*/
|
|
133
|
-
const computeCommonPrefix = (paths) => {
|
|
134
|
-
if (paths.length === 0)
|
|
135
|
-
return '';
|
|
136
|
-
if (paths.length === 1) {
|
|
137
|
-
// For a single path, extract the directory-like prefix
|
|
138
|
-
// e.g., /dynamic-routes/home -> /dynamic-routes
|
|
139
|
-
const segments = paths[0].split('/').filter(Boolean);
|
|
140
|
-
if (segments.length > 1) {
|
|
141
|
-
return '/' + segments.slice(0, -1).join('/');
|
|
142
|
-
}
|
|
143
|
-
return '/' + segments[0];
|
|
144
|
-
}
|
|
145
|
-
// Split all paths into segments
|
|
146
|
-
const segmentArrays = paths.map((p) => p.split('/').filter(Boolean));
|
|
147
|
-
const minLength = Math.min(...segmentArrays.map((s) => s.length));
|
|
148
|
-
const commonSegments = [];
|
|
149
|
-
for (let i = 0; i < minLength; i++) {
|
|
150
|
-
const segment = segmentArrays[0][i];
|
|
151
|
-
// Skip segments with route parameters or wildcards
|
|
152
|
-
if (segment.includes(':') || segment.includes('*')) {
|
|
153
|
-
break;
|
|
154
|
-
}
|
|
155
|
-
const allMatch = segmentArrays.every((s) => s[i] === segment);
|
|
156
|
-
if (allMatch) {
|
|
157
|
-
commonSegments.push(segment);
|
|
158
|
-
}
|
|
159
|
-
else {
|
|
160
|
-
break;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return commonSegments.length > 0 ? '/' + commonSegments.join('/') : '';
|
|
164
|
-
};
|
|
165
|
-
/**
|
|
166
|
-
* Checks if a pathname falls within the scope of a mount path using
|
|
167
|
-
* segment-aware comparison. Prevents false positives like "/tabs-secondary"
|
|
168
|
-
* matching mount path "/tabs".
|
|
169
|
-
*/
|
|
170
|
-
const isPathnameInScope = (pathname, mountPath) => {
|
|
171
|
-
if (mountPath === '/')
|
|
172
|
-
return true;
|
|
173
|
-
return pathname === mountPath || pathname.startsWith(mountPath + '/');
|
|
174
|
-
};
|
|
175
|
-
/**
|
|
176
|
-
* Checks if a route path is a "splat-only" route (just `*` or `/*`).
|
|
177
|
-
*/
|
|
178
|
-
const isSplatOnlyRoute = (routePath) => {
|
|
179
|
-
return routePath === '*' || routePath === '/*';
|
|
180
|
-
};
|
|
181
|
-
/**
|
|
182
|
-
* Checks if a route has an embedded wildcard (e.g., "tab1/*" but not "*" or "/*").
|
|
183
|
-
*/
|
|
184
|
-
const hasEmbeddedWildcard = (routePath) => {
|
|
185
|
-
return !!routePath && routePath.includes('*') && !isSplatOnlyRoute(routePath);
|
|
186
|
-
};
|
|
187
|
-
/**
|
|
188
|
-
* Checks if a route with an embedded wildcard matches a pathname.
|
|
189
|
-
*/
|
|
190
|
-
const matchesEmbeddedWildcardRoute = (route, pathname) => {
|
|
191
|
-
const routePath = route.props.path;
|
|
192
|
-
if (!hasEmbeddedWildcard(routePath)) {
|
|
193
|
-
return false;
|
|
194
|
-
}
|
|
195
|
-
return !!matchPath({ pathname, componentProps: route.props });
|
|
196
|
-
};
|
|
197
|
-
/**
|
|
198
|
-
* Checks if a route is a specific match (not wildcard-only or index).
|
|
199
|
-
*/
|
|
200
|
-
const isSpecificRouteMatch = (route, remainingPath) => {
|
|
201
|
-
const routePath = route.props.path;
|
|
202
|
-
if (route.props.index || isSplatOnlyRoute(routePath)) {
|
|
203
|
-
return false;
|
|
204
|
-
}
|
|
205
|
-
return !!matchPath({ pathname: remainingPath, componentProps: route.props });
|
|
206
|
-
};
|
|
207
|
-
/**
|
|
208
|
-
* Analyzes route children to determine their characteristics.
|
|
209
|
-
*
|
|
210
|
-
* @param routeChildren The route children to analyze.
|
|
211
|
-
* @returns Analysis of the route characteristics.
|
|
212
|
-
*/
|
|
213
|
-
const analyzeRouteChildren = (routeChildren) => {
|
|
214
|
-
const hasRelativeRoutes = routeChildren.some((route) => {
|
|
215
|
-
const path = route.props.path;
|
|
216
|
-
return path && !path.startsWith('/') && path !== '*';
|
|
217
|
-
});
|
|
218
|
-
const hasIndexRoute = routeChildren.some((route) => route.props.index);
|
|
219
|
-
const hasWildcardRoute = routeChildren.some((route) => {
|
|
220
|
-
const routePath = route.props.path;
|
|
221
|
-
return routePath === '*' || routePath === '/*';
|
|
222
|
-
});
|
|
223
|
-
return { hasRelativeRoutes, hasIndexRoute, hasWildcardRoute, routeChildren };
|
|
224
|
-
};
|
|
225
|
-
/**
|
|
226
|
-
* Checks if any route matches as a specific (non-wildcard, non-index) route.
|
|
227
|
-
*/
|
|
228
|
-
const findSpecificMatch = (routeChildren, remainingPath) => {
|
|
229
|
-
return routeChildren.some((route) => isSpecificRouteMatch(route, remainingPath) || matchesEmbeddedWildcardRoute(route, remainingPath));
|
|
230
|
-
};
|
|
231
|
-
/**
|
|
232
|
-
* Returns the first route that matches as a specific (non-wildcard, non-index) route.
|
|
233
|
-
*/
|
|
234
|
-
const findFirstSpecificMatchingRoute = (routeChildren, remainingPath) => {
|
|
235
|
-
return routeChildren.find((route) => isSpecificRouteMatch(route, remainingPath) || matchesEmbeddedWildcardRoute(route, remainingPath));
|
|
236
|
-
};
|
|
237
|
-
/**
|
|
238
|
-
* Checks if any specific route could plausibly match the remaining path.
|
|
239
|
-
* Used to determine if we should fall back to a wildcard match.
|
|
240
|
-
*
|
|
241
|
-
* Uses exact first-segment matching: the remaining path's first segment
|
|
242
|
-
* must exactly equal a route's first segment to block the wildcard.
|
|
243
|
-
* The outlet's mount path is always known from React Router's RouteContext,
|
|
244
|
-
* so no heuristic-based discovery is needed.
|
|
245
|
-
*/
|
|
246
|
-
const couldSpecificRouteMatch = (routeChildren, remainingPath) => {
|
|
247
|
-
const remainingFirstSegment = remainingPath.split('/')[0];
|
|
248
|
-
return routeChildren.some((route) => {
|
|
249
|
-
const routePath = route.props.path;
|
|
250
|
-
if (!routePath || routePath === '*' || routePath === '/*')
|
|
251
|
-
return false;
|
|
252
|
-
if (route.props.index)
|
|
253
|
-
return false;
|
|
254
|
-
const routeFirstSegment = routePath.split('/')[0].replace(/[*:]/g, '');
|
|
255
|
-
if (!routeFirstSegment)
|
|
256
|
-
return false;
|
|
257
|
-
return routeFirstSegment === remainingFirstSegment;
|
|
258
|
-
});
|
|
259
|
-
};
|
|
260
|
-
/**
|
|
261
|
-
* Determines the best parent path from the available matches.
|
|
262
|
-
* Priority: specific > wildcard > index
|
|
263
|
-
*/
|
|
264
|
-
const selectBestMatch = (specificMatch, wildcardMatch, indexMatch) => {
|
|
265
|
-
var _a;
|
|
266
|
-
return (_a = specificMatch !== null && specificMatch !== void 0 ? specificMatch : wildcardMatch) !== null && _a !== void 0 ? _a : indexMatch;
|
|
267
|
-
};
|
|
268
|
-
/**
|
|
269
|
-
* Handles outlets with only absolute routes by computing their common prefix.
|
|
270
|
-
*/
|
|
271
|
-
const computeAbsoluteRoutesParentPath = (routeChildren, currentPathname, outletMountPath) => {
|
|
272
|
-
const absolutePathRoutes = routeChildren.filter((route) => {
|
|
273
|
-
const path = route.props.path;
|
|
274
|
-
return path && path.startsWith('/');
|
|
275
|
-
});
|
|
276
|
-
if (absolutePathRoutes.length === 0) {
|
|
277
|
-
return undefined;
|
|
278
|
-
}
|
|
279
|
-
const absolutePaths = absolutePathRoutes.map((r) => r.props.path);
|
|
280
|
-
const commonPrefix = computeCommonPrefix(absolutePaths);
|
|
281
|
-
if (!commonPrefix || commonPrefix === '/') {
|
|
282
|
-
return undefined;
|
|
283
|
-
}
|
|
284
|
-
const newOutletMountPath = outletMountPath || commonPrefix;
|
|
285
|
-
if (!currentPathname.startsWith(commonPrefix)) {
|
|
286
|
-
return { parentPath: undefined, outletMountPath: newOutletMountPath };
|
|
287
|
-
}
|
|
288
|
-
return { parentPath: commonPrefix, outletMountPath: newOutletMountPath };
|
|
289
|
-
};
|
|
290
|
-
/**
|
|
291
|
-
* Computes the parent path for a nested outlet based on the current pathname
|
|
292
|
-
* and the outlet's route configuration.
|
|
293
|
-
*
|
|
294
|
-
* When the mount path is known (seeded from React Router's RouteContext), the
|
|
295
|
-
* parent path is simply the mount path — no iterative discovery needed. The
|
|
296
|
-
* iterative fallback only runs for outlets where RouteContext doesn't provide
|
|
297
|
-
* a parent match (typically root-level outlets on first render).
|
|
298
|
-
*
|
|
299
|
-
* @param options The options for computing the parent path.
|
|
300
|
-
* @returns The computed parent path result.
|
|
301
|
-
*/
|
|
302
|
-
const computeParentPath = (options) => {
|
|
303
|
-
const { currentPathname, outletMountPath, routeChildren, hasRelativeRoutes, hasIndexRoute, hasWildcardRoute } = options;
|
|
304
|
-
// If pathname is outside the established mount path scope, skip computation.
|
|
305
|
-
// Use segment-aware comparison: /tabs-secondary must NOT match /tabs scope.
|
|
306
|
-
if (outletMountPath && !isPathnameInScope(currentPathname, outletMountPath)) {
|
|
307
|
-
return { parentPath: undefined, outletMountPath };
|
|
308
|
-
}
|
|
309
|
-
// Fast path: when the mount path is known (from React Router's RouteContext),
|
|
310
|
-
// the parent path IS the mount path. The iterative segment-by-segment discovery
|
|
311
|
-
// below was needed when the mount depth had to be guessed from URL structure,
|
|
312
|
-
// but with RouteContext we already know exactly where this outlet is mounted.
|
|
313
|
-
if (outletMountPath && (hasRelativeRoutes || hasIndexRoute)) {
|
|
314
|
-
return { parentPath: outletMountPath, outletMountPath };
|
|
315
|
-
}
|
|
316
|
-
// Fallback: mount path not yet known. Iterate through path segments to discover
|
|
317
|
-
// the correct parent depth. This only runs on first render of outlets where
|
|
318
|
-
// RouteContext doesn't provide a parent match (typically root-level outlets,
|
|
319
|
-
// which usually have absolute routes and take the absolute routes path below).
|
|
320
|
-
if (!outletMountPath && (hasRelativeRoutes || hasIndexRoute) && currentPathname.includes('/')) {
|
|
321
|
-
const segments = currentPathname.split('/').filter(Boolean);
|
|
322
|
-
if (segments.length >= 1) {
|
|
323
|
-
let firstSpecificMatch;
|
|
324
|
-
let firstWildcardMatch;
|
|
325
|
-
let indexMatchAtMount;
|
|
326
|
-
for (let i = 1; i <= segments.length; i++) {
|
|
327
|
-
const parentPath = '/' + segments.slice(0, i).join('/');
|
|
328
|
-
const remainingPath = segments.slice(i).join('/');
|
|
329
|
-
// Check for specific route match (highest priority)
|
|
330
|
-
if (!firstSpecificMatch && findSpecificMatch(routeChildren, remainingPath)) {
|
|
331
|
-
// Don't let empty/default path routes (path="" or undefined) drive
|
|
332
|
-
// the parent deeper than a wildcard match. An empty path route matching
|
|
333
|
-
// when remainingPath is "" just means all segments were consumed.
|
|
334
|
-
if (firstWildcardMatch) {
|
|
335
|
-
const matchingRoute = findFirstSpecificMatchingRoute(routeChildren, remainingPath);
|
|
336
|
-
if (matchingRoute) {
|
|
337
|
-
const matchingPath = matchingRoute.props.path;
|
|
338
|
-
if (!matchingPath || matchingPath === '') {
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
firstSpecificMatch = parentPath;
|
|
344
|
-
break;
|
|
345
|
-
}
|
|
346
|
-
// Check for wildcard match (only if remaining path is non-empty)
|
|
347
|
-
const hasNonEmptyRemaining = remainingPath !== '' && remainingPath !== '/';
|
|
348
|
-
if (!firstWildcardMatch && hasNonEmptyRemaining && hasWildcardRoute) {
|
|
349
|
-
if (!couldSpecificRouteMatch(routeChildren, remainingPath)) {
|
|
350
|
-
firstWildcardMatch = parentPath;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
// Check for index route match
|
|
354
|
-
if ((remainingPath === '' || remainingPath === '/') && hasIndexRoute) {
|
|
355
|
-
indexMatchAtMount = parentPath;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
// Fallback: check root level for embedded wildcard routes (e.g., "tab1/*")
|
|
359
|
-
if (!firstSpecificMatch) {
|
|
360
|
-
const fullRemainingPath = segments.join('/');
|
|
361
|
-
if (routeChildren.some((route) => matchesEmbeddedWildcardRoute(route, fullRemainingPath))) {
|
|
362
|
-
firstSpecificMatch = '/';
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
const bestPath = selectBestMatch(firstSpecificMatch, firstWildcardMatch, indexMatchAtMount);
|
|
366
|
-
return { parentPath: bestPath, outletMountPath: bestPath };
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
// Handle outlets with only absolute routes
|
|
370
|
-
if (!hasRelativeRoutes && !hasIndexRoute) {
|
|
371
|
-
const result = computeAbsoluteRoutesParentPath(routeChildren, currentPathname, outletMountPath);
|
|
372
|
-
if (result) {
|
|
373
|
-
return result;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
return { parentPath: outletMountPath, outletMountPath };
|
|
377
|
-
};
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* Ensures the given path has a leading slash.
|
|
381
|
-
*
|
|
382
|
-
* @param value The path string to normalize.
|
|
383
|
-
* @returns The path with a leading slash.
|
|
384
|
-
*/
|
|
385
|
-
const ensureLeadingSlash = (value) => {
|
|
386
|
-
if (value === '') {
|
|
387
|
-
return '/';
|
|
388
|
-
}
|
|
389
|
-
return value.startsWith('/') ? value : `/${value}`;
|
|
390
|
-
};
|
|
391
|
-
/**
|
|
392
|
-
* Strips the trailing slash from a path, unless it's the root path.
|
|
393
|
-
*
|
|
394
|
-
* @param value The path string to normalize.
|
|
395
|
-
* @returns The path without a trailing slash.
|
|
396
|
-
*/
|
|
397
|
-
const stripTrailingSlash = (value) => {
|
|
398
|
-
return value.length > 1 && value.endsWith('/') ? value.slice(0, -1) : value;
|
|
399
|
-
};
|
|
400
|
-
/**
|
|
401
|
-
* Normalizes a pathname for comparison by ensuring a leading slash
|
|
402
|
-
* and removing trailing slashes.
|
|
403
|
-
*
|
|
404
|
-
* @param value The pathname to normalize, can be undefined.
|
|
405
|
-
* @returns A normalized pathname string.
|
|
406
|
-
*/
|
|
407
|
-
const normalizePathnameForComparison = (value) => {
|
|
408
|
-
if (!value || value === '') {
|
|
409
|
-
return '/';
|
|
410
|
-
}
|
|
411
|
-
const withLeadingSlash = ensureLeadingSlash(value);
|
|
412
|
-
return stripTrailingSlash(withLeadingSlash);
|
|
413
|
-
};
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* Extracts the children from a Routes wrapper component.
|
|
417
|
-
* The use of `<Routes />` is encouraged with React Router v6.
|
|
418
|
-
*
|
|
419
|
-
* @param node The React node to extract Routes children from.
|
|
420
|
-
* @returns The children of the Routes component, or undefined if not found.
|
|
421
|
-
*/
|
|
422
|
-
const getRoutesChildren = (node) => {
|
|
423
|
-
let routesNode;
|
|
424
|
-
React.Children.forEach(node, (child) => {
|
|
425
|
-
if (child.type === Routes) {
|
|
426
|
-
routesNode = child;
|
|
427
|
-
}
|
|
428
|
-
});
|
|
429
|
-
if (routesNode) {
|
|
430
|
-
// The children of the `<Routes />` component are most likely
|
|
431
|
-
// (and should be) the `<Route />` components.
|
|
432
|
-
return routesNode.props.children;
|
|
433
|
-
}
|
|
434
|
-
return undefined;
|
|
435
|
-
};
|
|
436
|
-
/**
|
|
437
|
-
* Extracts Route children from a node (either directly or from a Routes wrapper).
|
|
438
|
-
*
|
|
439
|
-
* @param children The children to extract routes from.
|
|
440
|
-
* @returns An array of Route elements.
|
|
441
|
-
*/
|
|
442
|
-
const extractRouteChildren = (children) => {
|
|
443
|
-
var _a;
|
|
444
|
-
const routesChildren = (_a = getRoutesChildren(children)) !== null && _a !== void 0 ? _a : children;
|
|
445
|
-
return React.Children.toArray(routesChildren).filter((child) => React.isValidElement(child) && (child.type === Route || child.type === IonRoute));
|
|
446
|
-
};
|
|
447
|
-
/**
|
|
448
|
-
* Checks if a React element is a Navigate component (redirect).
|
|
449
|
-
*
|
|
450
|
-
* @param element The element to check.
|
|
451
|
-
* @returns True if the element is a Navigate component.
|
|
452
|
-
*/
|
|
453
|
-
const isNavigateElement = (element) => {
|
|
454
|
-
return (React.isValidElement(element) &&
|
|
455
|
-
(element.type === Navigate || (typeof element.type === 'function' && element.type.name === 'Navigate')));
|
|
456
|
-
};
|
|
457
|
-
|
|
458
|
-
/**
|
|
459
|
-
* Compares two routes by specificity for sorting (most specific first).
|
|
460
|
-
*
|
|
461
|
-
* Sort order:
|
|
462
|
-
* 1. Index routes come first
|
|
463
|
-
* 2. Wildcard-only routes (* or /*) come last
|
|
464
|
-
* 3. Exact matches (no wildcards/params) before wildcard/param routes
|
|
465
|
-
* 4. Among routes with same status, longer paths are more specific
|
|
466
|
-
*/
|
|
467
|
-
const compareRouteSpecificity = (a, b) => {
|
|
468
|
-
// Index routes come first
|
|
469
|
-
if (a.index && !b.index)
|
|
470
|
-
return -1;
|
|
471
|
-
if (!a.index && b.index)
|
|
472
|
-
return 1;
|
|
473
|
-
// Wildcard-only routes (* or /*) should come last
|
|
474
|
-
const aIsWildcardOnly = a.path === '*' || a.path === '/*';
|
|
475
|
-
const bIsWildcardOnly = b.path === '*' || b.path === '/*';
|
|
476
|
-
if (!aIsWildcardOnly && bIsWildcardOnly)
|
|
477
|
-
return -1;
|
|
478
|
-
if (aIsWildcardOnly && !bIsWildcardOnly)
|
|
479
|
-
return 1;
|
|
480
|
-
// Exact matches (no wildcards/params) come before wildcard/param routes
|
|
481
|
-
const aHasWildcard = a.path.includes('*') || a.path.includes(':');
|
|
482
|
-
const bHasWildcard = b.path.includes('*') || b.path.includes(':');
|
|
483
|
-
if (!aHasWildcard && bHasWildcard)
|
|
484
|
-
return -1;
|
|
485
|
-
if (aHasWildcard && !bHasWildcard)
|
|
486
|
-
return 1;
|
|
487
|
-
// Among routes with same wildcard status, longer paths are more specific
|
|
488
|
-
if (a.path.length !== b.path.length) {
|
|
489
|
-
return b.path.length - a.path.length;
|
|
490
|
-
}
|
|
491
|
-
return 0;
|
|
492
|
-
};
|
|
493
|
-
/**
|
|
494
|
-
* Sorts view items by route specificity (most specific first).
|
|
495
|
-
*
|
|
496
|
-
* Sort order aligns with findViewItemByPath in ReactRouterViewStack.tsx:
|
|
497
|
-
* 1. Index routes come first
|
|
498
|
-
* 2. Wildcard-only routes (* or /*) come last
|
|
499
|
-
* 3. Exact matches (no wildcards/params) come before wildcard/param routes
|
|
500
|
-
* 4. Among routes with same wildcard status, longer paths are more specific
|
|
501
|
-
*
|
|
502
|
-
* @param views The view items to sort.
|
|
503
|
-
* @returns A new sorted array of view items.
|
|
504
|
-
*/
|
|
505
|
-
const sortViewsBySpecificity = (views) => {
|
|
506
|
-
return [...views].sort((a, b) => {
|
|
507
|
-
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
508
|
-
return compareRouteSpecificity({ path: ((_b = (_a = a.routeData) === null || _a === void 0 ? void 0 : _a.childProps) === null || _b === void 0 ? void 0 : _b.path) || '', index: !!((_d = (_c = a.routeData) === null || _c === void 0 ? void 0 : _c.childProps) === null || _d === void 0 ? void 0 : _d.index) }, { path: ((_f = (_e = b.routeData) === null || _e === void 0 ? void 0 : _e.childProps) === null || _f === void 0 ? void 0 : _f.path) || '', index: !!((_h = (_g = b.routeData) === null || _g === void 0 ? void 0 : _g.childProps) === null || _h === void 0 ? void 0 : _h.index) });
|
|
509
|
-
});
|
|
510
|
-
};
|
|
511
|
-
|
|
512
|
-
/**
|
|
513
|
-
* `ReactRouterViewStack` is a custom navigation manager used in Ionic React
|
|
514
|
-
* apps to map React Router route elements (such as `<IonRoute>`) to "view
|
|
515
|
-
* items" that Ionic can manage in a view stack. This is critical to maintain
|
|
516
|
-
* Ionic’s animation, lifecycle, and history behavior across views.
|
|
517
|
-
*/
|
|
518
|
-
/**
|
|
519
|
-
* Delay in milliseconds before removing a Navigate view item after a redirect.
|
|
520
|
-
* This ensures the redirect navigation completes before the view is removed.
|
|
521
|
-
*/
|
|
522
|
-
const NAVIGATE_REDIRECT_DELAY_MS = 100;
|
|
523
|
-
/**
|
|
524
|
-
* Delay in milliseconds before cleaning up a view without an IonPage element.
|
|
525
|
-
* This double-checks that the view is truly not needed before removal.
|
|
526
|
-
*/
|
|
527
|
-
const VIEW_CLEANUP_DELAY_MS = 200;
|
|
528
|
-
/**
|
|
529
|
-
* Computes the absolute pathnameBase for a route element based on its type.
|
|
530
|
-
* Handles relative paths, index routes, and splat routes differently.
|
|
531
|
-
*/
|
|
532
|
-
const computeAbsolutePathnameBase = (routeElement, routeMatch, parentPathnameBase, routeInfoPathname) => {
|
|
533
|
-
const routePath = routeElement.props.path;
|
|
534
|
-
const isRelativePath = routePath && !routePath.startsWith('/');
|
|
535
|
-
const isIndexRoute = !!routeElement.props.index;
|
|
536
|
-
const isSplatOnlyRoute = routePath === '*' || routePath === '/*';
|
|
537
|
-
if (isSplatOnlyRoute) {
|
|
538
|
-
// Splat routes should NOT contribute their matched portion to pathnameBase
|
|
539
|
-
// This aligns with React Router v7's v7_relativeSplatPath behavior
|
|
540
|
-
return parentPathnameBase;
|
|
541
|
-
}
|
|
542
|
-
if (isRelativePath && (routeMatch === null || routeMatch === void 0 ? void 0 : routeMatch.pathnameBase)) {
|
|
543
|
-
const relativeBase = routeMatch.pathnameBase.startsWith('/')
|
|
544
|
-
? routeMatch.pathnameBase.slice(1)
|
|
545
|
-
: routeMatch.pathnameBase;
|
|
546
|
-
return parentPathnameBase === '/' ? `/${relativeBase}` : `${parentPathnameBase}/${relativeBase}`;
|
|
547
|
-
}
|
|
548
|
-
if (isIndexRoute) {
|
|
549
|
-
return parentPathnameBase;
|
|
550
|
-
}
|
|
551
|
-
return (routeMatch === null || routeMatch === void 0 ? void 0 : routeMatch.pathnameBase) || routeInfoPathname;
|
|
552
|
-
};
|
|
553
|
-
/**
|
|
554
|
-
* Gets fallback params from view items in other outlets when parent context is empty.
|
|
555
|
-
* This handles cases where React context propagation doesn't work as expected.
|
|
556
|
-
*/
|
|
557
|
-
const getFallbackParamsFromViewItems = (allViewItems, currentOutletId, currentPathname) => {
|
|
558
|
-
var _a;
|
|
559
|
-
const matchingViews = [];
|
|
560
|
-
for (const otherViewItem of allViewItems) {
|
|
561
|
-
if (otherViewItem.outletId === currentOutletId)
|
|
562
|
-
continue;
|
|
563
|
-
const otherMatch = (_a = otherViewItem.routeData) === null || _a === void 0 ? void 0 : _a.match;
|
|
564
|
-
if ((otherMatch === null || otherMatch === void 0 ? void 0 : otherMatch.params) && Object.keys(otherMatch.params).length > 0) {
|
|
565
|
-
const matchedPathname = otherMatch.pathnameBase || otherMatch.pathname;
|
|
566
|
-
if (matchedPathname && currentPathname.startsWith(matchedPathname)) {
|
|
567
|
-
matchingViews.push({
|
|
568
|
-
params: otherMatch.params,
|
|
569
|
-
pathLength: matchedPathname.length,
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
// Sort ascending by path length so more-specific (longer) paths are applied
|
|
575
|
-
// last and their params take priority over less-specific ones.
|
|
576
|
-
matchingViews.sort((a, b) => a.pathLength - b.pathLength);
|
|
577
|
-
const params = {};
|
|
578
|
-
for (const view of matchingViews) {
|
|
579
|
-
Object.assign(params, view.params);
|
|
580
|
-
}
|
|
581
|
-
return params;
|
|
582
|
-
};
|
|
583
|
-
/**
|
|
584
|
-
* Builds the matches array for RouteContext.
|
|
585
|
-
*/
|
|
586
|
-
const buildContextMatches = (parentMatches, combinedParams, routeMatch, routeInfoPathname, absolutePathnameBase, viewItem, routeElement, componentElement) => {
|
|
587
|
-
return [
|
|
588
|
-
...parentMatches,
|
|
589
|
-
{
|
|
590
|
-
params: combinedParams,
|
|
591
|
-
pathname: (routeMatch === null || routeMatch === void 0 ? void 0 : routeMatch.pathname) || routeInfoPathname,
|
|
592
|
-
pathnameBase: absolutePathnameBase,
|
|
593
|
-
route: {
|
|
594
|
-
id: viewItem.id,
|
|
595
|
-
path: routeElement.props.path,
|
|
596
|
-
element: componentElement,
|
|
597
|
-
index: !!routeElement.props.index,
|
|
598
|
-
caseSensitive: routeElement.props.caseSensitive,
|
|
599
|
-
hasErrorBoundary: false,
|
|
600
|
-
},
|
|
601
|
-
},
|
|
602
|
-
];
|
|
603
|
-
};
|
|
604
|
-
const createDefaultMatch = (fullPathname, routeProps) => {
|
|
605
|
-
var _a, _b;
|
|
606
|
-
const isIndexRoute = !!routeProps.index;
|
|
607
|
-
const patternPath = (_a = routeProps.path) !== null && _a !== void 0 ? _a : '';
|
|
608
|
-
const pathnameBase = fullPathname === '' ? '/' : fullPathname;
|
|
609
|
-
const computedEnd = routeProps.end !== undefined ? routeProps.end : patternPath !== '' ? !patternPath.endsWith('*') : true;
|
|
610
|
-
return {
|
|
611
|
-
params: {},
|
|
612
|
-
pathname: isIndexRoute ? '' : fullPathname,
|
|
613
|
-
pathnameBase,
|
|
614
|
-
pattern: {
|
|
615
|
-
path: patternPath,
|
|
616
|
-
caseSensitive: (_b = routeProps.caseSensitive) !== null && _b !== void 0 ? _b : false,
|
|
617
|
-
end: isIndexRoute ? true : computedEnd,
|
|
618
|
-
},
|
|
619
|
-
};
|
|
620
|
-
};
|
|
621
|
-
const computeRelativeToParent = (pathname, parentPath) => {
|
|
622
|
-
if (!parentPath)
|
|
623
|
-
return null;
|
|
624
|
-
const normalizedParent = normalizePathnameForComparison(parentPath);
|
|
625
|
-
const normalizedPathname = normalizePathnameForComparison(pathname);
|
|
626
|
-
if (normalizedPathname === normalizedParent) {
|
|
627
|
-
return '';
|
|
628
|
-
}
|
|
629
|
-
const withSlash = normalizedParent === '/' ? '/' : normalizedParent + '/';
|
|
630
|
-
if (normalizedPathname.startsWith(withSlash)) {
|
|
631
|
-
return normalizedPathname.slice(withSlash.length);
|
|
632
|
-
}
|
|
633
|
-
return null;
|
|
634
|
-
};
|
|
635
|
-
const resolveIndexRouteMatch = (viewItem, pathname, parentPath) => {
|
|
636
|
-
var _a, _b, _c;
|
|
637
|
-
if (!((_b = (_a = viewItem.routeData) === null || _a === void 0 ? void 0 : _a.childProps) === null || _b === void 0 ? void 0 : _b.index)) {
|
|
638
|
-
return null;
|
|
639
|
-
}
|
|
640
|
-
// Prefer computing against the parent path when available to align with RRv6 semantics
|
|
641
|
-
const relative = computeRelativeToParent(pathname, parentPath);
|
|
642
|
-
if (relative !== null) {
|
|
643
|
-
// Index routes match only when there is no remaining path
|
|
644
|
-
if (relative === '' || relative === '/') {
|
|
645
|
-
return createDefaultMatch(parentPath || pathname, viewItem.routeData.childProps);
|
|
646
|
-
}
|
|
647
|
-
return null;
|
|
648
|
-
}
|
|
649
|
-
// Fallback: use previously computed match base for equality check
|
|
650
|
-
const previousMatch = (_c = viewItem.routeData) === null || _c === void 0 ? void 0 : _c.match;
|
|
651
|
-
if (!previousMatch) {
|
|
652
|
-
return null;
|
|
653
|
-
}
|
|
654
|
-
const normalizedPathname = normalizePathnameForComparison(pathname);
|
|
655
|
-
const normalizedBase = normalizePathnameForComparison(previousMatch.pathnameBase || previousMatch.pathname || '');
|
|
656
|
-
return normalizedPathname === normalizedBase ? previousMatch : null;
|
|
657
|
-
};
|
|
658
|
-
class ReactRouterViewStack extends ViewStacks {
|
|
659
|
-
constructor() {
|
|
660
|
-
super();
|
|
661
|
-
/**
|
|
662
|
-
* Stores the computed parent path for each outlet.
|
|
663
|
-
* Used by findViewItemByPath to correctly evaluate index route matches
|
|
664
|
-
* without requiring the outlet's React element or route children.
|
|
665
|
-
*/
|
|
666
|
-
this.outletParentPaths = new Map();
|
|
667
|
-
/**
|
|
668
|
-
* Stores the computed mount path for each outlet.
|
|
669
|
-
* Fed back into computeParentPath on subsequent calls to stabilize
|
|
670
|
-
* the parent path computation across navigations (mirrors StackManager.outletMountPath).
|
|
671
|
-
*/
|
|
672
|
-
this.outletMountPaths = new Map();
|
|
673
|
-
/**
|
|
674
|
-
* Creates a new view item for the given outlet and react route element.
|
|
675
|
-
* Associates route props with the matched route path for further lookups.
|
|
676
|
-
*/
|
|
677
|
-
this.createViewItem = (outletId, reactElement, routeInfo, page) => {
|
|
678
|
-
var _a, _b;
|
|
679
|
-
const routePath = reactElement.props.path || '';
|
|
680
|
-
// Check if we already have a view item for this exact route that we can reuse
|
|
681
|
-
// Include wildcard routes like tabs/* since they should be reused
|
|
682
|
-
// Also check unmounted items that might have been preserved for browser navigation
|
|
683
|
-
const existingViewItem = this.getViewItemsForOutlet(outletId).find((v) => {
|
|
684
|
-
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
685
|
-
const existingRouteProps = (_b = (_a = v.reactElement) === null || _a === void 0 ? void 0 : _a.props) !== null && _b !== void 0 ? _b : {};
|
|
686
|
-
const existingPath = existingRouteProps.path || '';
|
|
687
|
-
const existingElement = existingRouteProps.element;
|
|
688
|
-
const newElement = reactElement.props.element;
|
|
689
|
-
const existingIsIndexRoute = !!existingRouteProps.index;
|
|
690
|
-
const newIsIndexRoute = !!reactElement.props.index;
|
|
691
|
-
// For Navigate components, match by destination
|
|
692
|
-
const existingIsNavigate = React.isValidElement(existingElement) && existingElement.type === Navigate;
|
|
693
|
-
const newIsNavigate = React.isValidElement(newElement) && newElement.type === Navigate;
|
|
694
|
-
if (existingIsNavigate && newIsNavigate) {
|
|
695
|
-
const existingTo = (_c = existingElement.props) === null || _c === void 0 ? void 0 : _c.to;
|
|
696
|
-
const newTo = (_d = newElement.props) === null || _d === void 0 ? void 0 : _d.to;
|
|
697
|
-
if (existingTo === newTo) {
|
|
698
|
-
return true;
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
if (existingIsIndexRoute && newIsIndexRoute) {
|
|
702
|
-
return true;
|
|
703
|
-
}
|
|
704
|
-
// Reuse view items with the same path
|
|
705
|
-
// Special case: reuse tabs/* and other specific wildcard routes
|
|
706
|
-
// Don't reuse index routes (empty path) or generic catch-all wildcards (*)
|
|
707
|
-
if (existingPath === routePath && existingPath !== '' && existingPath !== '*') {
|
|
708
|
-
// Parameterized routes need pathname matching to ensure /details/1 and /details/2
|
|
709
|
-
// get separate view items. For wildcard routes (e.g., user/:userId/*), compare
|
|
710
|
-
// pathnameBase to allow child path changes while preserving the parent view.
|
|
711
|
-
const hasParams = routePath.includes(':');
|
|
712
|
-
const isWildcard = routePath.includes('*');
|
|
713
|
-
if (hasParams) {
|
|
714
|
-
if (isWildcard) {
|
|
715
|
-
const existingPathnameBase = (_f = (_e = v.routeData) === null || _e === void 0 ? void 0 : _e.match) === null || _f === void 0 ? void 0 : _f.pathnameBase;
|
|
716
|
-
const newMatch = matchComponent$1(reactElement, routeInfo.pathname, false, this.outletParentPaths.get(outletId));
|
|
717
|
-
const newPathnameBase = newMatch === null || newMatch === void 0 ? void 0 : newMatch.pathnameBase;
|
|
718
|
-
if (existingPathnameBase !== newPathnameBase) {
|
|
719
|
-
return false;
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
else {
|
|
723
|
-
const existingPathname = (_h = (_g = v.routeData) === null || _g === void 0 ? void 0 : _g.match) === null || _h === void 0 ? void 0 : _h.pathname;
|
|
724
|
-
if (existingPathname !== routeInfo.pathname) {
|
|
725
|
-
return false;
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
return true;
|
|
730
|
-
}
|
|
731
|
-
// Also reuse specific wildcard routes like tabs/*
|
|
732
|
-
if (existingPath === routePath && existingPath.endsWith('/*') && existingPath !== '/*') {
|
|
733
|
-
return true;
|
|
734
|
-
}
|
|
735
|
-
return false;
|
|
736
|
-
});
|
|
737
|
-
if (existingViewItem) {
|
|
738
|
-
// Update and ensure the existing view item is properly configured
|
|
739
|
-
existingViewItem.reactElement = reactElement;
|
|
740
|
-
existingViewItem.mount = true;
|
|
741
|
-
existingViewItem.ionPageElement = page || existingViewItem.ionPageElement;
|
|
742
|
-
const updatedMatch = matchComponent$1(reactElement, routeInfo.pathname, false, this.outletParentPaths.get(outletId)) ||
|
|
743
|
-
((_a = existingViewItem.routeData) === null || _a === void 0 ? void 0 : _a.match) ||
|
|
744
|
-
createDefaultMatch(routeInfo.pathname, reactElement.props);
|
|
745
|
-
existingViewItem.routeData = {
|
|
746
|
-
match: updatedMatch,
|
|
747
|
-
childProps: reactElement.props,
|
|
748
|
-
lastPathname: (_b = existingViewItem.routeData) === null || _b === void 0 ? void 0 : _b.lastPathname, // Preserve navigation history
|
|
749
|
-
};
|
|
750
|
-
return existingViewItem;
|
|
751
|
-
}
|
|
752
|
-
const id = `${outletId}-${generateId(outletId)}`;
|
|
753
|
-
const viewItem = {
|
|
754
|
-
id,
|
|
755
|
-
outletId,
|
|
756
|
-
ionPageElement: page,
|
|
757
|
-
reactElement,
|
|
758
|
-
mount: true,
|
|
759
|
-
ionRoute: true,
|
|
760
|
-
};
|
|
761
|
-
if (reactElement.type === IonRoute) {
|
|
762
|
-
viewItem.disableIonPageManagement = reactElement.props.disableIonPageManagement;
|
|
763
|
-
}
|
|
764
|
-
const initialMatch = matchComponent$1(reactElement, routeInfo.pathname, true, this.outletParentPaths.get(outletId)) ||
|
|
765
|
-
createDefaultMatch(routeInfo.pathname, reactElement.props);
|
|
766
|
-
viewItem.routeData = {
|
|
767
|
-
match: initialMatch,
|
|
768
|
-
childProps: reactElement.props,
|
|
769
|
-
};
|
|
770
|
-
this.add(viewItem);
|
|
771
|
-
return viewItem;
|
|
772
|
-
};
|
|
773
|
-
/**
|
|
774
|
-
* Renders a ViewLifeCycleManager for the given view item.
|
|
775
|
-
* Handles cleanup if the view no longer matches.
|
|
776
|
-
*
|
|
777
|
-
* - Deactivates view if it no longer matches the current route
|
|
778
|
-
* - Wraps the route element in <Routes> to support nested routing and ensure remounting
|
|
779
|
-
* - Adds a unique key to <Routes> so React Router remounts routes when switching
|
|
780
|
-
*/
|
|
781
|
-
this.renderViewItem = (viewItem, routeInfo, parentPath, reRender) => {
|
|
782
|
-
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
783
|
-
const routePath = viewItem.reactElement.props.path || '';
|
|
784
|
-
let match = matchComponent$1(viewItem.reactElement, routeInfo.pathname, false, parentPath);
|
|
785
|
-
if (!match) {
|
|
786
|
-
const indexMatch = resolveIndexRouteMatch(viewItem, routeInfo.pathname, parentPath);
|
|
787
|
-
if (indexMatch) {
|
|
788
|
-
match = indexMatch;
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
// For parameterized routes, check if this is a navigation to a different path instance
|
|
792
|
-
// In that case, we should NOT reuse this view - a new view should be created
|
|
793
|
-
const isParameterRoute = routePath.includes(':');
|
|
794
|
-
const previousMatch = (_a = viewItem.routeData) === null || _a === void 0 ? void 0 : _a.match;
|
|
795
|
-
const isSamePath = (match === null || match === void 0 ? void 0 : match.pathname) === (previousMatch === null || previousMatch === void 0 ? void 0 : previousMatch.pathname);
|
|
796
|
-
// Flag to indicate this view should not be reused for this different parameterized path
|
|
797
|
-
const shouldSkipForDifferentParam = isParameterRoute && match && previousMatch && !isSamePath;
|
|
798
|
-
// Don't deactivate views automatically - let the StackManager handle view lifecycle
|
|
799
|
-
// This preserves views in the stack for navigation history like native apps
|
|
800
|
-
// Views will be hidden/shown by the StackManager's transition logic instead of being unmounted
|
|
801
|
-
// Special handling for Navigate components - they should unmount after redirecting
|
|
802
|
-
const elementComponent = (_c = (_b = viewItem.reactElement) === null || _b === void 0 ? void 0 : _b.props) === null || _c === void 0 ? void 0 : _c.element;
|
|
803
|
-
const isNavigateComponent = isNavigateElement(elementComponent);
|
|
804
|
-
if (isNavigateComponent) {
|
|
805
|
-
// Navigate components should only be mounted when they match
|
|
806
|
-
// Once they redirect (no longer match), they should be removed completely
|
|
807
|
-
// IMPORTANT: For index routes, we need to check indexMatch too since matchComponent
|
|
808
|
-
// may not properly match index routes without explicit parent path context
|
|
809
|
-
const indexMatch = ((_e = (_d = viewItem.routeData) === null || _d === void 0 ? void 0 : _d.childProps) === null || _e === void 0 ? void 0 : _e.index)
|
|
810
|
-
? resolveIndexRouteMatch(viewItem, routeInfo.pathname, parentPath)
|
|
811
|
-
: null;
|
|
812
|
-
const hasValidMatch = match || indexMatch;
|
|
813
|
-
if (!hasValidMatch && viewItem.mount) {
|
|
814
|
-
viewItem.mount = false;
|
|
815
|
-
// Schedule removal of the Navigate view item after a short delay
|
|
816
|
-
// This ensures the redirect completes before removal
|
|
817
|
-
setTimeout(() => {
|
|
818
|
-
this.remove(viewItem);
|
|
819
|
-
reRender === null || reRender === void 0 ? void 0 : reRender();
|
|
820
|
-
}, NAVIGATE_REDIRECT_DELAY_MS);
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
// Components that don't have IonPage elements and no longer match should be cleaned up
|
|
824
|
-
// BUT we need to be careful not to remove them if they're part of browser navigation history
|
|
825
|
-
// This handles components that perform immediate actions like programmatic navigation
|
|
826
|
-
// EXCEPTION: Navigate components should ALWAYS remain mounted until they redirect
|
|
827
|
-
// since they need to be rendered to trigger the navigation
|
|
828
|
-
if (!match && viewItem.mount && !viewItem.ionPageElement && !isNavigateComponent) {
|
|
829
|
-
// Check if this view item should be preserved for browser navigation
|
|
830
|
-
// We'll keep it if it was recently active (within the last navigation)
|
|
831
|
-
const shouldPreserve = viewItem.routeData.lastPathname === routeInfo.pathname ||
|
|
832
|
-
((_f = viewItem.routeData.match) === null || _f === void 0 ? void 0 : _f.pathname) === routeInfo.lastPathname;
|
|
833
|
-
if (!shouldPreserve) {
|
|
834
|
-
// This view item doesn't match and doesn't have an IonPage
|
|
835
|
-
// It's likely a utility component that performs an action and navigates away
|
|
836
|
-
viewItem.mount = false;
|
|
837
|
-
// Schedule removal to allow it to be recreated on next navigation
|
|
838
|
-
setTimeout(() => {
|
|
839
|
-
// Double-check before removing - the view might be needed again
|
|
840
|
-
const stillNotNeeded = !viewItem.mount && !viewItem.ionPageElement;
|
|
841
|
-
if (stillNotNeeded) {
|
|
842
|
-
this.remove(viewItem);
|
|
843
|
-
reRender === null || reRender === void 0 ? void 0 : reRender();
|
|
844
|
-
}
|
|
845
|
-
}, VIEW_CLEANUP_DELAY_MS);
|
|
846
|
-
}
|
|
847
|
-
else {
|
|
848
|
-
// Preserve it but unmount it for now
|
|
849
|
-
viewItem.mount = false;
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
// Reactivate view if it matches but was previously deactivated
|
|
853
|
-
// Don't reactivate if this is a parameterized route navigating to a different path instance
|
|
854
|
-
// Don't reactivate catch-all wildcard routes — they are created fresh by createViewItem
|
|
855
|
-
const isCatchAllWildcard = routePath === '*' || routePath === '/*';
|
|
856
|
-
if (match && !viewItem.mount && !shouldSkipForDifferentParam && !isCatchAllWildcard) {
|
|
857
|
-
viewItem.mount = true;
|
|
858
|
-
viewItem.routeData.match = match;
|
|
859
|
-
}
|
|
860
|
-
// Deactivate wildcard (catch-all) and empty-path (default) routes when a more-specific route matches.
|
|
861
|
-
// This prevents "Not found" or fallback pages from showing alongside valid routes.
|
|
862
|
-
if (routePath === '*' || routePath === '') {
|
|
863
|
-
// Check if any other view in this outlet has a match for the current route
|
|
864
|
-
const outletViews = this.getViewItemsForOutlet(viewItem.outletId);
|
|
865
|
-
// When parent path context is available, compute the relative pathname once
|
|
866
|
-
// outside the loop since both routeInfo.pathname and parentPath are invariant.
|
|
867
|
-
const relativePathname = parentPath
|
|
868
|
-
? computeRelativeToParent(routeInfo.pathname, parentPath)
|
|
869
|
-
: null;
|
|
870
|
-
let hasSpecificMatch = outletViews.some((v) => {
|
|
871
|
-
var _a, _b;
|
|
872
|
-
if (v.id === viewItem.id)
|
|
873
|
-
return false; // Skip self
|
|
874
|
-
const vRoutePath = ((_b = (_a = v.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path) || '';
|
|
875
|
-
if (vRoutePath === '*' || vRoutePath === '')
|
|
876
|
-
return false; // Skip other wildcard/empty routes
|
|
877
|
-
// When parent path context is available and the route is relative, use
|
|
878
|
-
// parent-path-aware matching. This avoids false positives from
|
|
879
|
-
// derivePathnameToMatch's tail-slice heuristic, which can incorrectly
|
|
880
|
-
// match route literals that appear at the wrong position in the pathname.
|
|
881
|
-
// Example: pathname /parent/extra/details/99 with route details/:id —
|
|
882
|
-
// the tail-slice extracts ["details","99"] producing a false match.
|
|
883
|
-
if (parentPath && vRoutePath && !vRoutePath.startsWith('/')) {
|
|
884
|
-
if (relativePathname === null) {
|
|
885
|
-
return false; // Pathname is outside this outlet's parent scope
|
|
886
|
-
}
|
|
887
|
-
return !!matchPath({
|
|
888
|
-
pathname: relativePathname,
|
|
889
|
-
componentProps: v.reactElement.props,
|
|
890
|
-
});
|
|
891
|
-
}
|
|
892
|
-
// Fallback to matchComponent when no parent path context is available
|
|
893
|
-
const vMatch = v.reactElement ? matchComponent$1(v.reactElement, routeInfo.pathname) : null;
|
|
894
|
-
return !!vMatch;
|
|
895
|
-
});
|
|
896
|
-
// For catch-all * routes, also deactivate when the pathname matches the outlet's
|
|
897
|
-
// parent path exactly. This means there are no remaining segments for the wildcard
|
|
898
|
-
// to catch, so the empty-path or index route should handle it instead.
|
|
899
|
-
if (!hasSpecificMatch && routePath === '*') {
|
|
900
|
-
const outletParentPath = this.outletParentPaths.get(viewItem.outletId);
|
|
901
|
-
if (outletParentPath) {
|
|
902
|
-
const normalizedParent = normalizePathnameForComparison(outletParentPath);
|
|
903
|
-
const normalizedPathname = normalizePathnameForComparison(routeInfo.pathname);
|
|
904
|
-
if (normalizedPathname === normalizedParent) {
|
|
905
|
-
// Check if there's an empty-path or index view item that should handle this
|
|
906
|
-
const hasDefaultRoute = outletViews.some((v) => {
|
|
907
|
-
var _a, _b, _c, _d;
|
|
908
|
-
if (v.id === viewItem.id)
|
|
909
|
-
return false;
|
|
910
|
-
const vRoutePath = (_b = (_a = v.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path;
|
|
911
|
-
return vRoutePath === '' || vRoutePath === undefined || !!((_d = (_c = v.routeData) === null || _c === void 0 ? void 0 : _c.childProps) === null || _d === void 0 ? void 0 : _d.index);
|
|
912
|
-
});
|
|
913
|
-
if (hasDefaultRoute) {
|
|
914
|
-
hasSpecificMatch = true;
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
if (hasSpecificMatch) {
|
|
920
|
-
viewItem.mount = false;
|
|
921
|
-
if (viewItem.ionPageElement) {
|
|
922
|
-
viewItem.ionPageElement.classList.add('ion-page-hidden');
|
|
923
|
-
viewItem.ionPageElement.setAttribute('aria-hidden', 'true');
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
const routeElement = React.cloneElement(viewItem.reactElement);
|
|
928
|
-
const componentElement = routeElement.props.element;
|
|
929
|
-
// Don't update match for parameterized routes navigating to different path instances
|
|
930
|
-
// This preserves the original match so that findViewItemByPath can correctly skip this view
|
|
931
|
-
if (match && viewItem.routeData.match !== match && !shouldSkipForDifferentParam) {
|
|
932
|
-
viewItem.routeData.match = match;
|
|
933
|
-
}
|
|
934
|
-
const routeMatch = shouldSkipForDifferentParam ? (_g = viewItem.routeData) === null || _g === void 0 ? void 0 : _g.match : match || ((_h = viewItem.routeData) === null || _h === void 0 ? void 0 : _h.match);
|
|
935
|
-
return (React.createElement(UNSAFE_RouteContext.Consumer, { key: `view-context-${viewItem.id}` }, (parentContext) => {
|
|
936
|
-
var _a, _b;
|
|
937
|
-
const parentMatches = ((_a = parentContext === null || parentContext === void 0 ? void 0 : parentContext.matches) !== null && _a !== void 0 ? _a : []);
|
|
938
|
-
// Accumulate params from parent matches, with fallback to other outlets
|
|
939
|
-
let accumulatedParentParams = parentMatches.reduce((acc, m) => (Object.assign(Object.assign({}, acc), m.params)), {});
|
|
940
|
-
if (parentMatches.length === 0 && Object.keys(accumulatedParentParams).length === 0) {
|
|
941
|
-
accumulatedParentParams = getFallbackParamsFromViewItems(this.getAllViewItems(), viewItem.outletId, routeInfo.pathname);
|
|
942
|
-
}
|
|
943
|
-
const combinedParams = Object.assign(Object.assign({}, accumulatedParentParams), ((_b = routeMatch === null || routeMatch === void 0 ? void 0 : routeMatch.params) !== null && _b !== void 0 ? _b : {}));
|
|
944
|
-
const parentPathnameBase = parentMatches.length > 0 ? parentMatches[parentMatches.length - 1].pathnameBase : '/';
|
|
945
|
-
const absolutePathnameBase = computeAbsolutePathnameBase(routeElement, routeMatch, parentPathnameBase, routeInfo.pathname);
|
|
946
|
-
const contextMatches = buildContextMatches(parentMatches, combinedParams, routeMatch, routeInfo.pathname, absolutePathnameBase, viewItem, routeElement, componentElement);
|
|
947
|
-
const routeContextValue = parentContext
|
|
948
|
-
? Object.assign(Object.assign({}, parentContext), { matches: contextMatches }) : { outlet: null, matches: contextMatches, isDataRoute: false };
|
|
949
|
-
return (React.createElement(ViewLifeCycleManager, { key: `view-${viewItem.id}`, mount: viewItem.mount, removeView: () => this.remove(viewItem) },
|
|
950
|
-
React.createElement(UNSAFE_RouteContext.Provider, { value: routeContextValue }, componentElement)));
|
|
951
|
-
}));
|
|
952
|
-
};
|
|
953
|
-
/**
|
|
954
|
-
* Re-renders all active view items for the specified outlet.
|
|
955
|
-
* Ensures React elements are updated with the latest match.
|
|
956
|
-
*
|
|
957
|
-
* 1. Iterates through children of IonRouterOutlet
|
|
958
|
-
* 2. Updates each matching viewItem with the current child React element
|
|
959
|
-
* (important for updating props or changes to elements)
|
|
960
|
-
* 3. Returns a list of React components that will be rendered inside the outlet
|
|
961
|
-
* Each view is wrapped in <ViewLifeCycleManager> to manage lifecycle and rendering
|
|
962
|
-
*/
|
|
963
|
-
this.getChildrenToRender = (outletId, ionRouterOutlet, routeInfo, reRender, parentPathnameBase) => {
|
|
964
|
-
const viewItems = this.getViewItemsForOutlet(outletId);
|
|
965
|
-
// Seed the mount path from the parent route context if available.
|
|
966
|
-
// This provides the outlet's mount path immediately on first render,
|
|
967
|
-
// eliminating the need for heuristic-based discovery in computeParentPath.
|
|
968
|
-
if (parentPathnameBase && !this.outletMountPaths.has(outletId)) {
|
|
969
|
-
this.outletMountPaths.set(outletId, parentPathnameBase);
|
|
970
|
-
}
|
|
971
|
-
// Determine parentPath for outlets with relative or index routes.
|
|
972
|
-
// This populates outletParentPaths for findViewItemByPath's matchView
|
|
973
|
-
// and the catch-all deactivation logic in renderViewItem.
|
|
974
|
-
let parentPath = undefined;
|
|
975
|
-
try {
|
|
976
|
-
const routeChildren = extractRouteChildren(ionRouterOutlet.props.children);
|
|
977
|
-
const { hasRelativeRoutes, hasIndexRoute, hasWildcardRoute } = analyzeRouteChildren(routeChildren);
|
|
978
|
-
if (hasRelativeRoutes || hasIndexRoute) {
|
|
979
|
-
const result = computeParentPath({
|
|
980
|
-
currentPathname: routeInfo.pathname,
|
|
981
|
-
outletMountPath: this.outletMountPaths.get(outletId),
|
|
982
|
-
routeChildren,
|
|
983
|
-
hasRelativeRoutes,
|
|
984
|
-
hasIndexRoute,
|
|
985
|
-
hasWildcardRoute,
|
|
986
|
-
});
|
|
987
|
-
parentPath = result.parentPath;
|
|
988
|
-
// Persist the mount path for subsequent calls, mirroring StackManager.outletMountPath.
|
|
989
|
-
// Unlike outletParentPaths (cleared when parentPath is undefined), the mount path is
|
|
990
|
-
// intentionally sticky — it anchors the outlet's scope and is only removed in clear().
|
|
991
|
-
if (result.outletMountPath && !this.outletMountPaths.has(outletId)) {
|
|
992
|
-
this.outletMountPaths.set(outletId, result.outletMountPath);
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
catch (e) {
|
|
997
|
-
// Non-fatal: if we fail to compute parentPath, fall back to previous behavior
|
|
998
|
-
}
|
|
999
|
-
// Store the computed parentPath for use in findViewItemByPath.
|
|
1000
|
-
// Clear stale entries when parentPath is undefined (e.g., navigated out of scope).
|
|
1001
|
-
if (parentPath !== undefined) {
|
|
1002
|
-
this.outletParentPaths.set(outletId, parentPath);
|
|
1003
|
-
}
|
|
1004
|
-
else if (this.outletParentPaths.has(outletId)) {
|
|
1005
|
-
this.outletParentPaths.delete(outletId);
|
|
1006
|
-
}
|
|
1007
|
-
// Sync child elements with stored viewItems (e.g. to reflect new props)
|
|
1008
|
-
React.Children.forEach(ionRouterOutlet.props.children, (child) => {
|
|
1009
|
-
// Ensure the child is a valid React element since we
|
|
1010
|
-
// might have whitespace strings or other non-element children
|
|
1011
|
-
if (React.isValidElement(child)) {
|
|
1012
|
-
// Find view item by exact path match to avoid wildcard routes overwriting specific routes
|
|
1013
|
-
const childPath = child.props.path;
|
|
1014
|
-
const viewItem = viewItems.find((v) => {
|
|
1015
|
-
var _a, _b;
|
|
1016
|
-
const viewItemPath = (_b = (_a = v.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path;
|
|
1017
|
-
// Only update if paths match exactly (prevents wildcard routes from overwriting specific routes)
|
|
1018
|
-
return viewItemPath === childPath;
|
|
1019
|
-
});
|
|
1020
|
-
if (viewItem) {
|
|
1021
|
-
viewItem.reactElement = child;
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
});
|
|
1025
|
-
// Filter out duplicate view items by ID (but keep all mounted items)
|
|
1026
|
-
const uniqueViewItems = viewItems.filter((viewItem, index, array) => {
|
|
1027
|
-
// Remove duplicates by ID (keep first occurrence)
|
|
1028
|
-
const isFirstOccurrence = array.findIndex((v) => v.id === viewItem.id) === index;
|
|
1029
|
-
return isFirstOccurrence;
|
|
1030
|
-
});
|
|
1031
|
-
// Filter out unmounted Navigate components to prevent them from being rendered
|
|
1032
|
-
// and triggering unwanted redirects
|
|
1033
|
-
const renderableViewItems = uniqueViewItems.filter((viewItem) => {
|
|
1034
|
-
var _a, _b, _c, _d;
|
|
1035
|
-
const elementComponent = (_b = (_a = viewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.element;
|
|
1036
|
-
const isNavigateComponent = isNavigateElement(elementComponent);
|
|
1037
|
-
// Exclude unmounted Navigate components from rendering
|
|
1038
|
-
if (isNavigateComponent && !viewItem.mount) {
|
|
1039
|
-
return false;
|
|
1040
|
-
}
|
|
1041
|
-
// Filter out views that are unmounted, have no ionPageElement, and don't match the current route.
|
|
1042
|
-
// These are "stale" views from previous routes that should not be rendered.
|
|
1043
|
-
// Views WITH ionPageElement are handled by the normal lifecycle events.
|
|
1044
|
-
// Views that MATCH the current route should be kept (they might be transitioning).
|
|
1045
|
-
if (!viewItem.mount && !viewItem.ionPageElement) {
|
|
1046
|
-
// Check if this view's route path matches the current pathname
|
|
1047
|
-
const viewRoutePath = (_d = (_c = viewItem.reactElement) === null || _c === void 0 ? void 0 : _c.props) === null || _d === void 0 ? void 0 : _d.path;
|
|
1048
|
-
if (viewRoutePath) {
|
|
1049
|
-
// First try exact match using matchComponent
|
|
1050
|
-
const routeMatch = matchComponent$1(viewItem.reactElement, routeInfo.pathname, false, parentPath);
|
|
1051
|
-
if (routeMatch) {
|
|
1052
|
-
// View matches current route, keep it
|
|
1053
|
-
return true;
|
|
1054
|
-
}
|
|
1055
|
-
// For parent routes (like /multiple-tabs or /routing), check if current pathname
|
|
1056
|
-
// starts with this route's path. This handles views with IonSplitPane/IonTabs
|
|
1057
|
-
// that don't have IonPage but should remain mounted while navigating within their children.
|
|
1058
|
-
const normalizedViewPath = normalizePathnameForComparison(viewRoutePath.replace(/\/?\*$/, '')); // Remove trailing wildcard
|
|
1059
|
-
const normalizedCurrentPath = normalizePathnameForComparison(routeInfo.pathname);
|
|
1060
|
-
// Check if current pathname is within this view's route hierarchy
|
|
1061
|
-
const isWithinRouteHierarchy = normalizedCurrentPath === normalizedViewPath || normalizedCurrentPath.startsWith(normalizedViewPath + '/');
|
|
1062
|
-
if (!isWithinRouteHierarchy) {
|
|
1063
|
-
// View is outside current route hierarchy, remove it
|
|
1064
|
-
setTimeout(() => {
|
|
1065
|
-
this.remove(viewItem);
|
|
1066
|
-
reRender();
|
|
1067
|
-
}, 0);
|
|
1068
|
-
return false;
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
return true;
|
|
1073
|
-
});
|
|
1074
|
-
const renderedItems = renderableViewItems.map((viewItem) => this.renderViewItem(viewItem, routeInfo, parentPath, reRender));
|
|
1075
|
-
return renderedItems;
|
|
1076
|
-
};
|
|
1077
|
-
/**
|
|
1078
|
-
* Finds a view item matching the current route, optionally updating its match state.
|
|
1079
|
-
*/
|
|
1080
|
-
this.findViewItemByRouteInfo = (routeInfo, outletId, updateMatch) => {
|
|
1081
|
-
const { viewItem, match } = this.findViewItemByPath(routeInfo.pathname, outletId);
|
|
1082
|
-
const shouldUpdateMatch = updateMatch === undefined || updateMatch === true;
|
|
1083
|
-
if (shouldUpdateMatch && viewItem && match) {
|
|
1084
|
-
viewItem.routeData.match = match;
|
|
1085
|
-
}
|
|
1086
|
-
return viewItem;
|
|
1087
|
-
};
|
|
1088
|
-
/**
|
|
1089
|
-
* Finds the view item that was previously active before a route change.
|
|
1090
|
-
*/
|
|
1091
|
-
this.findLeavingViewItemByRouteInfo = (routeInfo, outletId, mustBeIonRoute = true) => {
|
|
1092
|
-
// If the lastPathname is not set, we cannot find a leaving view item
|
|
1093
|
-
if (!routeInfo.lastPathname) {
|
|
1094
|
-
return undefined;
|
|
1095
|
-
}
|
|
1096
|
-
const { viewItem } = this.findViewItemByPath(routeInfo.lastPathname, outletId, mustBeIonRoute);
|
|
1097
|
-
return viewItem;
|
|
1098
|
-
};
|
|
1099
|
-
/**
|
|
1100
|
-
* Finds a view item by pathname only, used in simpler queries.
|
|
1101
|
-
*/
|
|
1102
|
-
this.findViewItemByPathname = (pathname, outletId) => {
|
|
1103
|
-
const { viewItem } = this.findViewItemByPath(pathname, outletId);
|
|
1104
|
-
return viewItem;
|
|
1105
|
-
};
|
|
1106
|
-
/**
|
|
1107
|
-
* Clean up old, unmounted view items to prevent memory leaks
|
|
1108
|
-
*/
|
|
1109
|
-
this.cleanupStaleViewItems = (outletId) => {
|
|
1110
|
-
const viewItems = this.getViewItemsForOutlet(outletId);
|
|
1111
|
-
// Keep only the most recent mounted views and a few unmounted ones for history
|
|
1112
|
-
const maxUnmountedItems = 3;
|
|
1113
|
-
const unmountedItems = viewItems.filter((v) => !v.mount);
|
|
1114
|
-
if (unmountedItems.length > maxUnmountedItems) {
|
|
1115
|
-
// Remove oldest unmounted items
|
|
1116
|
-
const itemsToRemove = unmountedItems.slice(0, unmountedItems.length - maxUnmountedItems);
|
|
1117
|
-
itemsToRemove.forEach((item) => {
|
|
1118
|
-
this.remove(item);
|
|
1119
|
-
});
|
|
1120
|
-
}
|
|
1121
|
-
};
|
|
1122
|
-
/**
|
|
1123
|
-
* Override add to prevent duplicate view items with the same ID in the same outlet
|
|
1124
|
-
* But allow multiple view items for the same route path (for navigation history)
|
|
1125
|
-
*/
|
|
1126
|
-
this.add = (viewItem) => {
|
|
1127
|
-
const existingViewItem = this.getViewItemsForOutlet(viewItem.outletId).find((v) => v.id === viewItem.id);
|
|
1128
|
-
if (existingViewItem) {
|
|
1129
|
-
return;
|
|
1130
|
-
}
|
|
1131
|
-
super.add(viewItem);
|
|
1132
|
-
this.cleanupStaleViewItems(viewItem.outletId);
|
|
1133
|
-
};
|
|
1134
|
-
/**
|
|
1135
|
-
* Override clear to also clean up the stored parent path for the outlet.
|
|
1136
|
-
*/
|
|
1137
|
-
this.clear = (outletId) => {
|
|
1138
|
-
this.outletParentPaths.delete(outletId);
|
|
1139
|
-
this.outletMountPaths.delete(outletId);
|
|
1140
|
-
return super.clear(outletId);
|
|
1141
|
-
};
|
|
1142
|
-
/**
|
|
1143
|
-
* Override remove
|
|
1144
|
-
*/
|
|
1145
|
-
this.remove = (viewItem) => {
|
|
1146
|
-
super.remove(viewItem);
|
|
1147
|
-
};
|
|
1148
|
-
}
|
|
1149
|
-
/**
|
|
1150
|
-
* Core function that matches a given pathname against all view items.
|
|
1151
|
-
* Returns both the matched view item and match metadata.
|
|
1152
|
-
*/
|
|
1153
|
-
findViewItemByPath(pathname, outletId, mustBeIonRoute, allowDefaultMatch = true) {
|
|
1154
|
-
let viewItem;
|
|
1155
|
-
let match = null;
|
|
1156
|
-
let viewStack;
|
|
1157
|
-
// Capture stored parent paths for use in nested matchView/matchDefaultRoute functions
|
|
1158
|
-
const storedParentPaths = this.outletParentPaths;
|
|
1159
|
-
if (outletId) {
|
|
1160
|
-
viewStack = sortViewsBySpecificity(this.getViewItemsForOutlet(outletId));
|
|
1161
|
-
viewStack.some(matchView);
|
|
1162
|
-
if (!viewItem && allowDefaultMatch)
|
|
1163
|
-
viewStack.some(matchDefaultRoute);
|
|
1164
|
-
}
|
|
1165
|
-
else {
|
|
1166
|
-
const viewItems = sortViewsBySpecificity(this.getAllViewItems());
|
|
1167
|
-
viewItems.some(matchView);
|
|
1168
|
-
if (!viewItem && allowDefaultMatch)
|
|
1169
|
-
viewItems.some(matchDefaultRoute);
|
|
1170
|
-
}
|
|
1171
|
-
// If we still have not found a view item for this outlet, try to find a matching
|
|
1172
|
-
// view item across all outlets and adopt it into the current outlet. This helps
|
|
1173
|
-
// recover when an outlet remounts and receives a new id, leaving views associated
|
|
1174
|
-
// with the previous outlet id.
|
|
1175
|
-
// Do not adopt across outlets; if we didn't find a view for this outlet,
|
|
1176
|
-
// defer to route matching to create a new one.
|
|
1177
|
-
return { viewItem, match };
|
|
1178
|
-
/**
|
|
1179
|
-
* Matches a route path with dynamic parameters (e.g. /tabs/:id)
|
|
1180
|
-
*/
|
|
1181
|
-
function matchView(v) {
|
|
1182
|
-
var _a;
|
|
1183
|
-
if (mustBeIonRoute && !v.ionRoute)
|
|
1184
|
-
return false;
|
|
1185
|
-
const viewItemPath = v.routeData.childProps.path || '';
|
|
1186
|
-
// Skip unmounted catch-all wildcard views. After back navigation unmounts
|
|
1187
|
-
// a wildcard view, it should not be reused for subsequent navigations.
|
|
1188
|
-
// A fresh wildcard view will be created by createViewItem when needed.
|
|
1189
|
-
if ((viewItemPath === '*' || viewItemPath === '/*') && !v.mount)
|
|
1190
|
-
return false;
|
|
1191
|
-
const isIndexRoute = !!v.routeData.childProps.index;
|
|
1192
|
-
const previousMatch = (_a = v.routeData) === null || _a === void 0 ? void 0 : _a.match;
|
|
1193
|
-
const outletParentPath = storedParentPaths.get(v.outletId);
|
|
1194
|
-
const result = v.reactElement ? matchComponent$1(v.reactElement, pathname, false, outletParentPath) : null;
|
|
1195
|
-
if (!result) {
|
|
1196
|
-
const indexMatch = resolveIndexRouteMatch(v, pathname, outletParentPath);
|
|
1197
|
-
if (indexMatch) {
|
|
1198
|
-
match = indexMatch;
|
|
1199
|
-
viewItem = v;
|
|
1200
|
-
return true;
|
|
1201
|
-
}
|
|
1202
|
-
// Empty path routes (path="") should match when the pathname matches the
|
|
1203
|
-
// outlet's parent path exactly (no remaining segments). matchComponent doesn't
|
|
1204
|
-
// handle this because it lacks parent path context. Without this check, a
|
|
1205
|
-
// catch-all * view item (which matches any pathname) would be incorrectly
|
|
1206
|
-
// returned instead of the empty path route on back navigation.
|
|
1207
|
-
if (viewItemPath === '' && !isIndexRoute && outletParentPath) {
|
|
1208
|
-
const normalizedParent = normalizePathnameForComparison(outletParentPath);
|
|
1209
|
-
const normalizedPathname = normalizePathnameForComparison(pathname);
|
|
1210
|
-
if (normalizedPathname === normalizedParent) {
|
|
1211
|
-
match = createDefaultMatch(pathname, v.routeData.childProps);
|
|
1212
|
-
viewItem = v;
|
|
1213
|
-
return true;
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
if (result) {
|
|
1218
|
-
const hasParams = result.params && Object.keys(result.params).length > 0;
|
|
1219
|
-
const isSamePath = result.pathname === (previousMatch === null || previousMatch === void 0 ? void 0 : previousMatch.pathname);
|
|
1220
|
-
const isWildcardRoute = viewItemPath.includes('*');
|
|
1221
|
-
const isParameterRoute = viewItemPath.includes(':');
|
|
1222
|
-
// Don't allow view items with undefined paths to match specific routes
|
|
1223
|
-
// This prevents broken index route view items from interfering with navigation
|
|
1224
|
-
if (!viewItemPath && !isIndexRoute && pathname !== '/' && pathname !== '') {
|
|
1225
|
-
return false;
|
|
1226
|
-
}
|
|
1227
|
-
// For parameterized routes, check if we should reuse the view item.
|
|
1228
|
-
// Wildcard routes (e.g., user/:userId/*) compare pathnameBase to allow
|
|
1229
|
-
// child path changes while preserving the parent view.
|
|
1230
|
-
if (isParameterRoute && !isSamePath) {
|
|
1231
|
-
if (isWildcardRoute) {
|
|
1232
|
-
const isSameBase = result.pathnameBase === (previousMatch === null || previousMatch === void 0 ? void 0 : previousMatch.pathnameBase);
|
|
1233
|
-
if (isSameBase) {
|
|
1234
|
-
match = result;
|
|
1235
|
-
viewItem = v;
|
|
1236
|
-
return true;
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
return false;
|
|
1240
|
-
}
|
|
1241
|
-
// For routes without params, or when navigating to the exact same path,
|
|
1242
|
-
// or when there's no previous match, reuse the view item
|
|
1243
|
-
if (!hasParams || isSamePath || !previousMatch) {
|
|
1244
|
-
match = result;
|
|
1245
|
-
viewItem = v;
|
|
1246
|
-
return true;
|
|
1247
|
-
}
|
|
1248
|
-
// For pure wildcard routes (without : params), compare pathnameBase to allow
|
|
1249
|
-
// child path changes while preserving the parent view. This handles container
|
|
1250
|
-
// routes like /tabs/* where switching between /tabs/tab1 and /tabs/tab2
|
|
1251
|
-
// should reuse the same ViewItem.
|
|
1252
|
-
if (isWildcardRoute && !isParameterRoute) {
|
|
1253
|
-
const isSameBase = result.pathnameBase === (previousMatch === null || previousMatch === void 0 ? void 0 : previousMatch.pathnameBase);
|
|
1254
|
-
if (isSameBase) {
|
|
1255
|
-
match = result;
|
|
1256
|
-
viewItem = v;
|
|
1257
|
-
return true;
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
return false;
|
|
1262
|
-
}
|
|
1263
|
-
/**
|
|
1264
|
-
* Matches a view with no path prop (default fallback route) or index route.
|
|
1265
|
-
*/
|
|
1266
|
-
function matchDefaultRoute(v) {
|
|
1267
|
-
var _a, _b, _c;
|
|
1268
|
-
const childProps = v.routeData.childProps;
|
|
1269
|
-
const isDefaultRoute = childProps.path === undefined || childProps.path === '';
|
|
1270
|
-
const isIndexRoute = !!childProps.index;
|
|
1271
|
-
if (isIndexRoute) {
|
|
1272
|
-
const outletParentPath = storedParentPaths.get(v.outletId);
|
|
1273
|
-
const indexMatch = resolveIndexRouteMatch(v, pathname, outletParentPath);
|
|
1274
|
-
if (indexMatch) {
|
|
1275
|
-
match = indexMatch;
|
|
1276
|
-
viewItem = v;
|
|
1277
|
-
return true;
|
|
1278
|
-
}
|
|
1279
|
-
return false;
|
|
1280
|
-
}
|
|
1281
|
-
// For empty path routes, only match if we're at the same level as when the view was created.
|
|
1282
|
-
// This prevents an empty path view item from being reused for different routes.
|
|
1283
|
-
if (isDefaultRoute) {
|
|
1284
|
-
const previousPathnameBase = ((_b = (_a = v.routeData) === null || _a === void 0 ? void 0 : _a.match) === null || _b === void 0 ? void 0 : _b.pathnameBase) || '';
|
|
1285
|
-
const normalizedBase = normalizePathnameForComparison(previousPathnameBase);
|
|
1286
|
-
const normalizedPathname = normalizePathnameForComparison(pathname);
|
|
1287
|
-
if (normalizedPathname !== normalizedBase) {
|
|
1288
|
-
return false;
|
|
1289
|
-
}
|
|
1290
|
-
match = {
|
|
1291
|
-
params: {},
|
|
1292
|
-
pathname,
|
|
1293
|
-
pathnameBase: pathname === '' ? '/' : pathname,
|
|
1294
|
-
pattern: {
|
|
1295
|
-
path: '',
|
|
1296
|
-
caseSensitive: (_c = childProps.caseSensitive) !== null && _c !== void 0 ? _c : false,
|
|
1297
|
-
end: true,
|
|
1298
|
-
},
|
|
1299
|
-
};
|
|
1300
|
-
viewItem = v;
|
|
1301
|
-
return true;
|
|
1302
|
-
}
|
|
1303
|
-
return false;
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
/**
|
|
1308
|
-
* Utility to apply matchPath to a React element and return its match state.
|
|
1309
|
-
*/
|
|
1310
|
-
function matchComponent$1(node, pathname, allowFallback = false, parentPath) {
|
|
1311
|
-
var _a;
|
|
1312
|
-
const routeProps = (_a = node === null || node === void 0 ? void 0 : node.props) !== null && _a !== void 0 ? _a : {};
|
|
1313
|
-
const routePath = routeProps.path;
|
|
1314
|
-
let pathnameToMatch;
|
|
1315
|
-
if (parentPath && routePath && !routePath.startsWith('/')) {
|
|
1316
|
-
// When parent path is known, compute exact relative pathname
|
|
1317
|
-
// instead of using the tail-slice heuristic
|
|
1318
|
-
const relative = pathname.startsWith(parentPath)
|
|
1319
|
-
? pathname.slice(parentPath.length).replace(/^\//, '')
|
|
1320
|
-
: pathname;
|
|
1321
|
-
pathnameToMatch = relative;
|
|
1322
|
-
}
|
|
1323
|
-
else {
|
|
1324
|
-
pathnameToMatch = derivePathnameToMatch(pathname, routePath);
|
|
1325
|
-
}
|
|
1326
|
-
const match = matchPath({
|
|
1327
|
-
pathname: pathnameToMatch,
|
|
1328
|
-
componentProps: routeProps,
|
|
1329
|
-
});
|
|
1330
|
-
if (match || !allowFallback) {
|
|
1331
|
-
return match;
|
|
1332
|
-
}
|
|
1333
|
-
const isIndexRoute = !!routeProps.index;
|
|
1334
|
-
if (isIndexRoute) {
|
|
1335
|
-
return createDefaultMatch(pathname, routeProps);
|
|
1336
|
-
}
|
|
1337
|
-
if (!routePath || routePath === '') {
|
|
1338
|
-
return createDefaultMatch(pathname, routeProps);
|
|
1339
|
-
}
|
|
1340
|
-
return null;
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
function clonePageElement(leavingViewHtml) {
|
|
1344
|
-
let html;
|
|
1345
|
-
if (typeof leavingViewHtml === 'string') {
|
|
1346
|
-
html = leavingViewHtml;
|
|
1347
|
-
}
|
|
1348
|
-
else {
|
|
1349
|
-
html = leavingViewHtml.outerHTML;
|
|
1350
|
-
}
|
|
1351
|
-
if (document) {
|
|
1352
|
-
const newEl = document.createElement('div');
|
|
1353
|
-
newEl.innerHTML = html;
|
|
1354
|
-
newEl.style.zIndex = '';
|
|
1355
|
-
// Remove an existing back button so the new element doesn't get two of them
|
|
1356
|
-
const ionBackButton = newEl.getElementsByTagName('ion-back-button');
|
|
1357
|
-
if (ionBackButton[0]) {
|
|
1358
|
-
ionBackButton[0].remove();
|
|
1359
|
-
}
|
|
1360
|
-
return newEl.firstChild;
|
|
1361
|
-
}
|
|
1362
|
-
return undefined;
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
/**
|
|
1366
|
-
* `StackManager` is responsible for managing page transitions, keeping track
|
|
1367
|
-
* of views (pages), and ensuring that navigation behaves like native apps —
|
|
1368
|
-
* particularly with animations and swipe gestures.
|
|
1369
|
-
*/
|
|
1370
|
-
/**
|
|
1371
|
-
* Delay in milliseconds before unmounting a view after a transition completes.
|
|
1372
|
-
* This ensures the page transition animation finishes before the view is removed.
|
|
1373
|
-
*/
|
|
1374
|
-
const VIEW_UNMOUNT_DELAY_MS = 250;
|
|
1375
|
-
/**
|
|
1376
|
-
* Delay (ms) to wait for an IonPage to mount before proceeding with a
|
|
1377
|
-
* page transition. Only container routes (nested outlets with no direct
|
|
1378
|
-
* IonPage) actually hit this timeout; normal routes clear it early via
|
|
1379
|
-
* registerIonPage, so a larger value here doesn't affect the happy path.
|
|
1380
|
-
*/
|
|
1381
|
-
const ION_PAGE_WAIT_TIMEOUT_MS = 300;
|
|
1382
|
-
const isViewVisible = (el) => !el.classList.contains('ion-page-invisible') && !el.classList.contains('ion-page-hidden') && el.style.visibility !== 'hidden';
|
|
1383
|
-
const hideIonPageElement = (element) => {
|
|
1384
|
-
var _a;
|
|
1385
|
-
if (element) {
|
|
1386
|
-
if (element.id === 'section-a' || element.id === 'section-b') {
|
|
1387
|
-
// eslint-disable-next-line no-console
|
|
1388
|
-
console.log('[HideIonPageElement]', JSON.stringify({
|
|
1389
|
-
id: element.id,
|
|
1390
|
-
stack: (_a = new Error().stack) === null || _a === void 0 ? void 0 : _a.split('\n').slice(1, 6).map((s) => s.trim()),
|
|
1391
|
-
}));
|
|
1392
|
-
}
|
|
1393
|
-
element.classList.add('ion-page-hidden');
|
|
1394
|
-
element.setAttribute('aria-hidden', 'true');
|
|
1395
|
-
}
|
|
1396
|
-
};
|
|
1397
|
-
const showIonPageElement = (element) => {
|
|
1398
|
-
if (element) {
|
|
1399
|
-
element.style.removeProperty('visibility');
|
|
1400
|
-
// core transitions (md.transition.ts, ios.transition.ts) leave inline styles
|
|
1401
|
-
// on the leaving page after animation: `display: none` plus the final
|
|
1402
|
-
// keyframe values for `transform` and `opacity` (MD only). Preserved views
|
|
1403
|
-
// must clear all of these on re-entry so they render in the correct
|
|
1404
|
-
// position when the back direction entering animation is a no-op.
|
|
1405
|
-
element.style.removeProperty('display');
|
|
1406
|
-
element.style.removeProperty('transform');
|
|
1407
|
-
element.style.removeProperty('opacity');
|
|
1408
|
-
element.classList.remove('ion-page-hidden');
|
|
1409
|
-
element.removeAttribute('aria-hidden');
|
|
1410
|
-
}
|
|
1411
|
-
};
|
|
1412
|
-
/**
|
|
1413
|
-
* Variant of `showIonPageElement` for the swipe-back gesture start. Clears
|
|
1414
|
-
* `display: none` and the hidden class/attribute so the entering view is
|
|
1415
|
-
* visible, but intentionally keeps any inline `transform` and `opacity` set
|
|
1416
|
-
* by core's prior forward transition. The gesture's progress animation starts
|
|
1417
|
-
* from that pose, so clearing them here would cause a visible jump before
|
|
1418
|
-
* core's progress animation takes over.
|
|
1419
|
-
*/
|
|
1420
|
-
const revealIonPageForSwipeBack = (element) => {
|
|
1421
|
-
if (element) {
|
|
1422
|
-
const before = {
|
|
1423
|
-
id: element.id,
|
|
1424
|
-
inlineDisplay: element.style.display,
|
|
1425
|
-
hasHiddenClass: element.classList.contains('ion-page-hidden'),
|
|
1426
|
-
ariaHidden: element.getAttribute('aria-hidden'),
|
|
1427
|
-
computedDisplay: getComputedStyle(element).display,
|
|
1428
|
-
};
|
|
1429
|
-
element.style.removeProperty('display');
|
|
1430
|
-
element.classList.remove('ion-page-hidden');
|
|
1431
|
-
element.removeAttribute('aria-hidden');
|
|
1432
|
-
// eslint-disable-next-line no-console
|
|
1433
|
-
console.log('[SwipeBackReveal]', JSON.stringify({
|
|
1434
|
-
before,
|
|
1435
|
-
after: {
|
|
1436
|
-
inlineDisplay: element.style.display,
|
|
1437
|
-
hasHiddenClass: element.classList.contains('ion-page-hidden'),
|
|
1438
|
-
ariaHidden: element.getAttribute('aria-hidden'),
|
|
1439
|
-
computedDisplay: getComputedStyle(element).display,
|
|
1440
|
-
},
|
|
1441
|
-
}));
|
|
1442
|
-
}
|
|
1443
|
-
else {
|
|
1444
|
-
// eslint-disable-next-line no-console
|
|
1445
|
-
console.log('[SwipeBackReveal] element is undefined');
|
|
1446
|
-
}
|
|
1447
|
-
};
|
|
1448
|
-
/**
|
|
1449
|
-
* A leaf view is "preservable" on browser-back (pop) when its React state
|
|
1450
|
-
* should survive a forward-pop round-trip. Non-parameterized leaf paths
|
|
1451
|
-
* resolve to the same view item on re-entry, so keeping them mounted retains
|
|
1452
|
-
* user-visible state (scroll, inputs, cleared lists, etc.).
|
|
1453
|
-
*
|
|
1454
|
-
* Excluded:
|
|
1455
|
-
* - Parameterized routes (`/users/:id`): each param value gets a distinct
|
|
1456
|
-
* view item, so preserving them accumulates hidden views in the DOM.
|
|
1457
|
-
* - Wildcard container routes (`/tabs/*`, `*`): wrap nested outlets and must
|
|
1458
|
-
* be destroyed so nested outlet state rebuilds cleanly on re-entry.
|
|
1459
|
-
*/
|
|
1460
|
-
const isViewItemPreservableOnPop = (viewItem) => {
|
|
1461
|
-
var _a, _b;
|
|
1462
|
-
const path = (_b = (_a = viewItem === null || viewItem === void 0 ? void 0 : viewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path;
|
|
1463
|
-
if (!path) {
|
|
1464
|
-
return false;
|
|
1465
|
-
}
|
|
1466
|
-
if (path === '*' || path.endsWith('/*')) {
|
|
1467
|
-
return false;
|
|
1468
|
-
}
|
|
1469
|
-
return !path.includes(':');
|
|
1470
|
-
};
|
|
1471
|
-
class StackManager extends React.PureComponent {
|
|
1472
|
-
constructor(props) {
|
|
1473
|
-
super(props);
|
|
1474
|
-
this.stackContextValue = {
|
|
1475
|
-
registerIonPage: this.registerIonPage.bind(this),
|
|
1476
|
-
isInOutlet: () => true,
|
|
1477
|
-
};
|
|
1478
|
-
this.pendingPageTransition = false;
|
|
1479
|
-
this.waitingForIonPage = false;
|
|
1480
|
-
/** Whether this outlet was previously in scope. */
|
|
1481
|
-
this.wasInScope = true;
|
|
1482
|
-
/**
|
|
1483
|
-
* Views that have been explicitly kept alive by the pop-preserve logic
|
|
1484
|
-
* (shouldPreserveLeavingView) so a future forward-pop can restore their React
|
|
1485
|
-
* state. These are candidates for cleanup when a fresh push invalidates the
|
|
1486
|
-
* forward-history path that made them reachable. Views mounted through
|
|
1487
|
-
* normal forward-push (which keeps the leaving view alive by default) are
|
|
1488
|
-
* NOT tracked here.
|
|
1489
|
-
*/
|
|
1490
|
-
this.preservedViewItems = new Set();
|
|
1491
|
-
/** Tracks whether the component is mounted to guard async transition paths. */
|
|
1492
|
-
this._isMounted = false;
|
|
1493
|
-
/** In-flight requestAnimationFrame IDs from transitionPage, cancelled on unmount. */
|
|
1494
|
-
this.transitionRafIds = [];
|
|
1495
|
-
/**
|
|
1496
|
-
* Monotonically increasing counter incremented at the start of each transitionPage call.
|
|
1497
|
-
* Used to detect when an async commit() resolves after a newer transition has already run,
|
|
1498
|
-
* preventing the stale commit from hiding an element that the newer transition made visible.
|
|
1499
|
-
*/
|
|
1500
|
-
this.transitionGeneration = 0;
|
|
1501
|
-
this.outletMountPath = undefined;
|
|
1502
|
-
/**
|
|
1503
|
-
* Whether this outlet is at the root level (no parent route matches).
|
|
1504
|
-
* Derived from UNSAFE_RouteContext in render() — empty matches means root.
|
|
1505
|
-
*/
|
|
1506
|
-
this.isRootOutlet = true;
|
|
1507
|
-
this.registerIonPage = this.registerIonPage.bind(this);
|
|
1508
|
-
this.transitionPage = this.transitionPage.bind(this);
|
|
1509
|
-
this.handlePageTransition = this.handlePageTransition.bind(this);
|
|
1510
|
-
this.id = props.id || `routerOutlet-${generateId('routerOutlet')}`;
|
|
1511
|
-
this.prevProps = undefined;
|
|
1512
|
-
this.skipTransition = false;
|
|
1513
|
-
}
|
|
1514
|
-
/**
|
|
1515
|
-
* Determines the parent path for nested routing in React Router 6.
|
|
1516
|
-
*
|
|
1517
|
-
* When the mount path is known (seeded from UNSAFE_RouteContext), returns
|
|
1518
|
-
* it directly — no iterative discovery needed. The computeParentPath
|
|
1519
|
-
* fallback only runs for root outlets where RouteContext doesn't provide
|
|
1520
|
-
* a parent match.
|
|
1521
|
-
*/
|
|
1522
|
-
getParentPath() {
|
|
1523
|
-
const currentPathname = this.props.routeInfo.pathname;
|
|
1524
|
-
// Prevent out-of-scope outlets from adopting unrelated routes.
|
|
1525
|
-
// Uses segment-aware comparison: /tabs-secondary must NOT match /tabs scope.
|
|
1526
|
-
if (this.outletMountPath && !isPathnameInScope(currentPathname, this.outletMountPath)) {
|
|
1527
|
-
return undefined;
|
|
1528
|
-
}
|
|
1529
|
-
// Fast path: mount path is known from RouteContext. The parent path IS the
|
|
1530
|
-
// mount path — no need to run the iterative computeParentPath algorithm.
|
|
1531
|
-
if (this.outletMountPath && !this.isRootOutlet) {
|
|
1532
|
-
return this.outletMountPath;
|
|
1533
|
-
}
|
|
1534
|
-
// Fallback: root outlet or mount path not yet seeded. Run the full
|
|
1535
|
-
// computeParentPath algorithm to discover the parent depth.
|
|
1536
|
-
if (this.ionRouterOutlet) {
|
|
1537
|
-
const routeChildren = extractRouteChildren(this.ionRouterOutlet.props.children);
|
|
1538
|
-
const { hasRelativeRoutes, hasIndexRoute, hasWildcardRoute } = analyzeRouteChildren(routeChildren);
|
|
1539
|
-
if (!this.isRootOutlet || hasRelativeRoutes || hasIndexRoute) {
|
|
1540
|
-
const result = computeParentPath({
|
|
1541
|
-
currentPathname,
|
|
1542
|
-
outletMountPath: this.outletMountPath,
|
|
1543
|
-
routeChildren,
|
|
1544
|
-
hasRelativeRoutes,
|
|
1545
|
-
hasIndexRoute,
|
|
1546
|
-
hasWildcardRoute,
|
|
1547
|
-
});
|
|
1548
|
-
if (result.outletMountPath && !this.outletMountPath) {
|
|
1549
|
-
this.outletMountPath = result.outletMountPath;
|
|
1550
|
-
}
|
|
1551
|
-
return result.parentPath;
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
return this.outletMountPath;
|
|
1555
|
-
}
|
|
1556
|
-
/**
|
|
1557
|
-
* Finds the entering and leaving view items, handling redirect cases.
|
|
1558
|
-
*/
|
|
1559
|
-
findViewItems(routeInfo) {
|
|
1560
|
-
const enteringViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id);
|
|
1561
|
-
let leavingViewItem = this.context.findLeavingViewItemByRouteInfo(routeInfo, this.id);
|
|
1562
|
-
// Try to find leaving view by previous pathname
|
|
1563
|
-
if (!leavingViewItem && routeInfo.prevRouteLastPathname) {
|
|
1564
|
-
leavingViewItem = this.context.findViewItemByPathname(routeInfo.prevRouteLastPathname, this.id);
|
|
1565
|
-
}
|
|
1566
|
-
// For redirects where entering === leaving, find the actual previous view
|
|
1567
|
-
if (enteringViewItem &&
|
|
1568
|
-
leavingViewItem &&
|
|
1569
|
-
enteringViewItem === leavingViewItem &&
|
|
1570
|
-
routeInfo.routeAction === 'replace' &&
|
|
1571
|
-
routeInfo.prevRouteLastPathname) {
|
|
1572
|
-
const actualLeavingView = this.context.findViewItemByPathname(routeInfo.prevRouteLastPathname, this.id);
|
|
1573
|
-
if (actualLeavingView && actualLeavingView !== enteringViewItem) {
|
|
1574
|
-
leavingViewItem = actualLeavingView;
|
|
1575
|
-
}
|
|
1576
|
-
}
|
|
1577
|
-
// Handle redirect scenario with no leaving view
|
|
1578
|
-
if (enteringViewItem &&
|
|
1579
|
-
!leavingViewItem &&
|
|
1580
|
-
routeInfo.routeAction === 'replace' &&
|
|
1581
|
-
routeInfo.prevRouteLastPathname) {
|
|
1582
|
-
const actualLeavingView = this.context.findViewItemByPathname(routeInfo.prevRouteLastPathname, this.id);
|
|
1583
|
-
if (actualLeavingView && actualLeavingView !== enteringViewItem) {
|
|
1584
|
-
leavingViewItem = actualLeavingView;
|
|
1585
|
-
}
|
|
1586
|
-
}
|
|
1587
|
-
return { enteringViewItem, leavingViewItem };
|
|
1588
|
-
}
|
|
1589
|
-
shouldUnmountLeavingView(routeInfo, enteringViewItem, leavingViewItem) {
|
|
1590
|
-
var _a, _b;
|
|
1591
|
-
if (!leavingViewItem) {
|
|
1592
|
-
return false;
|
|
1593
|
-
}
|
|
1594
|
-
if (routeInfo.routeAction === 'replace') {
|
|
1595
|
-
const leavingRoutePath = (_b = (_a = leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path;
|
|
1596
|
-
// Never unmount root path or views without a path - needed for back navigation
|
|
1597
|
-
if (!leavingRoutePath || leavingRoutePath === '/' || leavingRoutePath === '') {
|
|
1598
|
-
return false;
|
|
1599
|
-
}
|
|
1600
|
-
// Replace actions unmount the leaving view since it's being replaced in history.
|
|
1601
|
-
return true;
|
|
1602
|
-
}
|
|
1603
|
-
// For non-replace actions, only unmount for back navigation
|
|
1604
|
-
const isForwardPush = routeInfo.routeAction === 'push' && routeInfo.routeDirection === 'forward';
|
|
1605
|
-
if (!isForwardPush && routeInfo.routeDirection !== 'none' && enteringViewItem !== leavingViewItem) {
|
|
1606
|
-
return true;
|
|
1607
|
-
}
|
|
1608
|
-
return false;
|
|
1609
|
-
}
|
|
1610
|
-
/**
|
|
1611
|
-
* Handles out-of-scope outlet. Returns true if transition should be aborted.
|
|
1612
|
-
*/
|
|
1613
|
-
handleOutOfScopeOutlet(routeInfo) {
|
|
1614
|
-
var _a, _b, _c;
|
|
1615
|
-
if (!this.outletMountPath || isPathnameInScope(routeInfo.pathname, this.outletMountPath)) {
|
|
1616
|
-
this.wasInScope = true;
|
|
1617
|
-
// Cancel any pending deferred unmount from a previous out-of-scope transition.
|
|
1618
|
-
if (this.outOfScopeUnmountTimeout) {
|
|
1619
|
-
clearTimeout(this.outOfScopeUnmountTimeout);
|
|
1620
|
-
this.outOfScopeUnmountTimeout = undefined;
|
|
1621
|
-
}
|
|
1622
|
-
return false;
|
|
1623
|
-
}
|
|
1624
|
-
// Only run the out-of-scope cleanup on the first transition out of scope.
|
|
1625
|
-
// When parameterized routes create multiple StackManager instances with the
|
|
1626
|
-
// same outlet ID, a stale (hidden) instance must not destroy views that an
|
|
1627
|
-
// active instance just created. After the initial cleanup, the stale instance
|
|
1628
|
-
// stays dormant until its mount path becomes in-scope again.
|
|
1629
|
-
if (!this.wasInScope) {
|
|
1630
|
-
return true;
|
|
1631
|
-
}
|
|
1632
|
-
this.wasInScope = false;
|
|
1633
|
-
// For ionPage outlets whose parent outlet has swipe-to-go-back enabled,
|
|
1634
|
-
// preserve child views so they remain visible during the swipe gesture.
|
|
1635
|
-
// Without this, the deferred unmount removes child pages before the gesture
|
|
1636
|
-
// starts, showing an empty shell when swiping back. Views are cleaned up
|
|
1637
|
-
// when the parent outlet pops this view (componentWillUnmount -> clearOutlet).
|
|
1638
|
-
//
|
|
1639
|
-
// Lifecycle events are skipped in this branch because the view stays mounted
|
|
1640
|
-
// and visible. Firing ionViewWillLeave/DidLeave here would be asymmetric with
|
|
1641
|
-
// no matching ionViewWillEnter/DidEnter when the view comes back in scope.
|
|
1642
|
-
const isIonPageOutlet = (_a = this.routerOutletElement) === null || _a === void 0 ? void 0 : _a.classList.contains('ion-page');
|
|
1643
|
-
if (isIonPageOutlet) {
|
|
1644
|
-
const parentOutlet = (_c = (_b = this.routerOutletElement) === null || _b === void 0 ? void 0 : _b.parentElement) === null || _c === void 0 ? void 0 : _c.closest('ion-router-outlet');
|
|
1645
|
-
if ((parentOutlet === null || parentOutlet === void 0 ? void 0 : parentOutlet.swipeGesture) === true) {
|
|
1646
|
-
this.dismissPresentedOverlays();
|
|
1647
|
-
return true;
|
|
1648
|
-
}
|
|
1649
|
-
}
|
|
1650
|
-
// Fire lifecycle events on any visible view before unmounting.
|
|
1651
|
-
// When navigating away from a tabbed section, the parent outlet fires
|
|
1652
|
-
// ionViewDidLeave on the tabs container, but the active tab child page
|
|
1653
|
-
// never receives its own lifecycle events because the core transition
|
|
1654
|
-
// dispatches events with bubbles:false. This ensures tab child pages
|
|
1655
|
-
// get ionViewWillLeave/ionViewDidLeave so useIonViewDidLeave fires.
|
|
1656
|
-
const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id);
|
|
1657
|
-
allViewsInOutlet.forEach((viewItem) => {
|
|
1658
|
-
if (viewItem.ionPageElement && isViewVisible(viewItem.ionPageElement)) {
|
|
1659
|
-
viewItem.ionPageElement.dispatchEvent(new CustomEvent('ionViewWillLeave', { bubbles: false, cancelable: false }));
|
|
1660
|
-
viewItem.ionPageElement.dispatchEvent(new CustomEvent('ionViewDidLeave', { bubbles: false, cancelable: false }));
|
|
1661
|
-
}
|
|
1662
|
-
});
|
|
1663
|
-
// Defer removal of view items to allow the parent outlet's leaving-page
|
|
1664
|
-
// animation to complete with content still visible. When the nested outlet
|
|
1665
|
-
// unmounts views immediately, React removes the child DOM elements before
|
|
1666
|
-
// the parent's transition animation can render them. On MD mode the back
|
|
1667
|
-
// animation only animates the leaving page (slide down + fade), so an
|
|
1668
|
-
// empty shell is invisible and the transition appears instant.
|
|
1669
|
-
//
|
|
1670
|
-
// VIEW_UNMOUNT_DELAY_MS exceeds the MD back transition (200ms).
|
|
1671
|
-
this.outOfScopeUnmountTimeout = setTimeout(() => {
|
|
1672
|
-
if (!this._isMounted)
|
|
1673
|
-
return;
|
|
1674
|
-
allViewsInOutlet.forEach((viewItem) => {
|
|
1675
|
-
this.context.unMountViewItem(viewItem);
|
|
1676
|
-
});
|
|
1677
|
-
this.forceUpdate();
|
|
1678
|
-
}, VIEW_UNMOUNT_DELAY_MS);
|
|
1679
|
-
return true;
|
|
1680
|
-
}
|
|
1681
|
-
/**
|
|
1682
|
-
* Handles root navigation by unmounting all non-entering views in this outlet.
|
|
1683
|
-
* Fires ionViewWillLeave / ionViewDidLeave only on views that are currently visible.
|
|
1684
|
-
* Views that are mounted but not visible (e.g., pages earlier in the back stack)
|
|
1685
|
-
* are silently unmounted without lifecycle events, consistent with the behavior
|
|
1686
|
-
* of out-of-scope outlet cleanup.
|
|
1687
|
-
*/
|
|
1688
|
-
handleRootNavigation(enteringViewItem) {
|
|
1689
|
-
const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id);
|
|
1690
|
-
allViewsInOutlet.forEach((viewItem) => {
|
|
1691
|
-
if (viewItem === enteringViewItem) {
|
|
1692
|
-
return;
|
|
1693
|
-
}
|
|
1694
|
-
if (viewItem.ionPageElement && isViewVisible(viewItem.ionPageElement)) {
|
|
1695
|
-
viewItem.ionPageElement.dispatchEvent(new CustomEvent('ionViewWillLeave', { bubbles: false, cancelable: false }));
|
|
1696
|
-
viewItem.ionPageElement.dispatchEvent(new CustomEvent('ionViewDidLeave', { bubbles: false, cancelable: false }));
|
|
1697
|
-
}
|
|
1698
|
-
this.context.unMountViewItem(viewItem);
|
|
1699
|
-
});
|
|
1700
|
-
}
|
|
1701
|
-
/**
|
|
1702
|
-
* Handles nested outlet with relative routes but no parent path. Returns true to abort.
|
|
1703
|
-
*/
|
|
1704
|
-
handleOutOfContextNestedOutlet(parentPath, leavingViewItem) {
|
|
1705
|
-
var _a;
|
|
1706
|
-
if (this.isRootOutlet || parentPath !== undefined || !this.ionRouterOutlet) {
|
|
1707
|
-
return false;
|
|
1708
|
-
}
|
|
1709
|
-
const routesChildren = (_a = getRoutesChildren(this.ionRouterOutlet.props.children)) !== null && _a !== void 0 ? _a : this.ionRouterOutlet.props.children;
|
|
1710
|
-
const routeChildren = React.Children.toArray(routesChildren).filter((child) => React.isValidElement(child) && (child.type === Route || child.type === IonRoute));
|
|
1711
|
-
const hasRelativeRoutes = routeChildren.some((route) => {
|
|
1712
|
-
const path = route.props.path;
|
|
1713
|
-
return path && !path.startsWith('/') && path !== '*';
|
|
1714
|
-
});
|
|
1715
|
-
if (hasRelativeRoutes) {
|
|
1716
|
-
hideIonPageElement(leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.ionPageElement);
|
|
1717
|
-
if (leavingViewItem) {
|
|
1718
|
-
leavingViewItem.mount = false;
|
|
1719
|
-
}
|
|
1720
|
-
this.forceUpdate();
|
|
1721
|
-
return true;
|
|
1722
|
-
}
|
|
1723
|
-
return false;
|
|
1724
|
-
}
|
|
1725
|
-
/**
|
|
1726
|
-
* Handles nested outlet with no matching route. Returns true to abort.
|
|
1727
|
-
*/
|
|
1728
|
-
handleNoMatchingRoute(enteringRoute, enteringViewItem, leavingViewItem) {
|
|
1729
|
-
if (this.isRootOutlet || enteringRoute || enteringViewItem) {
|
|
1730
|
-
return false;
|
|
1731
|
-
}
|
|
1732
|
-
hideIonPageElement(leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.ionPageElement);
|
|
1733
|
-
if (leavingViewItem) {
|
|
1734
|
-
leavingViewItem.mount = false;
|
|
1735
|
-
}
|
|
1736
|
-
this.forceUpdate();
|
|
1737
|
-
return true;
|
|
1738
|
-
}
|
|
1739
|
-
/**
|
|
1740
|
-
* Handles transition when entering view has ion-page element ready.
|
|
1741
|
-
*/
|
|
1742
|
-
handleReadyEnteringView(routeInfo, enteringViewItem, leavingViewItem, shouldUnmountLeavingViewItem) {
|
|
1743
|
-
var _a, _b;
|
|
1744
|
-
const routePath = (_b = (_a = enteringViewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path;
|
|
1745
|
-
const isParameterizedRoute = routePath ? routePath.includes(':') : false;
|
|
1746
|
-
const isWildcardContainerRoute = routePath ? routePath.endsWith('/*') : false;
|
|
1747
|
-
// Handle same-view transitions (parameterized routes like /user/:id or container routes like /tabs/*)
|
|
1748
|
-
// When entering === leaving, the view is already visible - skip transition to prevent flash
|
|
1749
|
-
if (enteringViewItem === leavingViewItem) {
|
|
1750
|
-
if (isParameterizedRoute || isWildcardContainerRoute) {
|
|
1751
|
-
const updatedMatch = matchComponent(enteringViewItem.reactElement, routeInfo.pathname, true, this.outletMountPath);
|
|
1752
|
-
if (updatedMatch) {
|
|
1753
|
-
enteringViewItem.routeData.match = updatedMatch;
|
|
1754
|
-
}
|
|
1755
|
-
const enteringEl = enteringViewItem.ionPageElement;
|
|
1756
|
-
if (enteringEl) {
|
|
1757
|
-
showIonPageElement(enteringEl);
|
|
1758
|
-
enteringEl.classList.remove('ion-page-invisible');
|
|
1759
|
-
// Maintain can-go-back state since we skip transitionPage/commit.
|
|
1760
|
-
// Without this, the back button disappears on re-navigation to a
|
|
1761
|
-
// parameterized route within a nested outlet (e.g. welcome -> item -> back -> item).
|
|
1762
|
-
if (routeInfo.pushedByRoute) {
|
|
1763
|
-
enteringEl.classList.add('can-go-back');
|
|
1764
|
-
}
|
|
1765
|
-
else {
|
|
1766
|
-
enteringEl.classList.remove('can-go-back');
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
this.forceUpdate();
|
|
1770
|
-
return;
|
|
1771
|
-
}
|
|
1772
|
-
}
|
|
1773
|
-
// For wildcard container routes, check if we're navigating within the same container.
|
|
1774
|
-
// If both the current pathname and the previous pathname match the same container route,
|
|
1775
|
-
// skip the transition - the nested outlet will handle the actual page change.
|
|
1776
|
-
// This handles cases where leavingViewItem lookup fails (e.g., no IonPage wrapper).
|
|
1777
|
-
if (isWildcardContainerRoute && routeInfo.lastPathname) {
|
|
1778
|
-
// routePath is guaranteed to exist since isWildcardContainerRoute checks routePath?.endsWith('/*')
|
|
1779
|
-
const containerBase = routePath.replace(/\/\*$/, '');
|
|
1780
|
-
const currentInContainer = routeInfo.pathname.startsWith(containerBase + '/') || routeInfo.pathname === containerBase;
|
|
1781
|
-
const previousInContainer = routeInfo.lastPathname.startsWith(containerBase + '/') || routeInfo.lastPathname === containerBase;
|
|
1782
|
-
if (currentInContainer && previousInContainer) {
|
|
1783
|
-
const updatedMatch = matchComponent(enteringViewItem.reactElement, routeInfo.pathname, true, this.outletMountPath);
|
|
1784
|
-
if (updatedMatch) {
|
|
1785
|
-
enteringViewItem.routeData.match = updatedMatch;
|
|
1786
|
-
}
|
|
1787
|
-
this.forceUpdate();
|
|
1788
|
-
return;
|
|
1789
|
-
}
|
|
1790
|
-
}
|
|
1791
|
-
if (!leavingViewItem && this.props.routeInfo.prevRouteLastPathname) {
|
|
1792
|
-
leavingViewItem = this.context.findViewItemByPathname(this.props.routeInfo.prevRouteLastPathname, this.id);
|
|
1793
|
-
}
|
|
1794
|
-
// Re-mount views that were previously unmounted (e.g., navigating back to home)
|
|
1795
|
-
if (!enteringViewItem.mount) {
|
|
1796
|
-
enteringViewItem.mount = true;
|
|
1797
|
-
}
|
|
1798
|
-
// A view that becomes the entering view is no longer a stale preserved view.
|
|
1799
|
-
// It's back in the active navigation path, so drop it from the cleanup set.
|
|
1800
|
-
this.preservedViewItems.delete(enteringViewItem);
|
|
1801
|
-
// Check visibility state BEFORE showing entering view
|
|
1802
|
-
const enteringWasVisible = enteringViewItem.ionPageElement && isViewVisible(enteringViewItem.ionPageElement);
|
|
1803
|
-
const leavingIsHidden = leavingViewItem !== undefined && leavingViewItem.ionPageElement && !isViewVisible(leavingViewItem.ionPageElement);
|
|
1804
|
-
const currentTransition = {
|
|
1805
|
-
enteringId: enteringViewItem.id,
|
|
1806
|
-
leavingId: leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.id,
|
|
1807
|
-
};
|
|
1808
|
-
const isDuplicateTransition = leavingViewItem &&
|
|
1809
|
-
this.lastTransition &&
|
|
1810
|
-
this.lastTransition.leavingId &&
|
|
1811
|
-
this.lastTransition.enteringId === currentTransition.enteringId &&
|
|
1812
|
-
this.lastTransition.leavingId === currentTransition.leavingId;
|
|
1813
|
-
// Skip if transition already performed (e.g., via swipe gesture)
|
|
1814
|
-
if (enteringWasVisible && leavingIsHidden && isDuplicateTransition) {
|
|
1815
|
-
if (this.skipTransition &&
|
|
1816
|
-
shouldUnmountLeavingViewItem &&
|
|
1817
|
-
leavingViewItem &&
|
|
1818
|
-
enteringViewItem !== leavingViewItem) {
|
|
1819
|
-
leavingViewItem.mount = false;
|
|
1820
|
-
// Trigger ionViewDidLeave lifecycle for ViewLifeCycleManager cleanup
|
|
1821
|
-
this.transitionPage(routeInfo, enteringViewItem, leavingViewItem, 'back');
|
|
1822
|
-
}
|
|
1823
|
-
this.skipTransition = false;
|
|
1824
|
-
this.forceUpdate();
|
|
1825
|
-
return;
|
|
1826
|
-
}
|
|
1827
|
-
showIonPageElement(enteringViewItem.ionPageElement);
|
|
1828
|
-
// Handle duplicate transition or swipe gesture completion
|
|
1829
|
-
if (isDuplicateTransition || this.skipTransition) {
|
|
1830
|
-
if (this.skipTransition &&
|
|
1831
|
-
shouldUnmountLeavingViewItem &&
|
|
1832
|
-
leavingViewItem &&
|
|
1833
|
-
enteringViewItem !== leavingViewItem) {
|
|
1834
|
-
leavingViewItem.mount = false;
|
|
1835
|
-
// Re-fire ionViewDidLeave since gesture completed before mount=false was set
|
|
1836
|
-
this.transitionPage(routeInfo, enteringViewItem, leavingViewItem, 'back');
|
|
1837
|
-
}
|
|
1838
|
-
this.skipTransition = false;
|
|
1839
|
-
this.forceUpdate();
|
|
1840
|
-
return;
|
|
1841
|
-
}
|
|
1842
|
-
this.lastTransition = currentTransition;
|
|
1843
|
-
const shouldSkipAnimation = this.applySkipAnimationIfNeeded(enteringViewItem, leavingViewItem);
|
|
1844
|
-
this.transitionPage(routeInfo, enteringViewItem, leavingViewItem, undefined, false, shouldSkipAnimation);
|
|
1845
|
-
if (shouldUnmountLeavingViewItem && leavingViewItem && enteringViewItem !== leavingViewItem) {
|
|
1846
|
-
// For replace actions, skip setting mount=false here. handleLeavingViewUnmount
|
|
1847
|
-
// sets it only after its container-to-container guard passes, avoiding zombie state.
|
|
1848
|
-
//
|
|
1849
|
-
// For pop (browser back) on preservable routes (see isViewItemPreservableOnPop),
|
|
1850
|
-
// keep the view alive (hidden by the transition) so its React state survives a
|
|
1851
|
-
// forward-pop round-trip. ionViewDidLeave already fired via the transitionPage()
|
|
1852
|
-
// call above. Swipe-to-go-back still destroys views through the skipTransition
|
|
1853
|
-
// path earlier in this method, matching native gesture behavior.
|
|
1854
|
-
//
|
|
1855
|
-
// handleLeavingViewUnmount below is a no-op for non-replace actions (early return),
|
|
1856
|
-
// so pop-preserved views pass through it untouched.
|
|
1857
|
-
const shouldPreserveLeavingView = routeInfo.routeAction === 'pop' && isViewItemPreservableOnPop(leavingViewItem);
|
|
1858
|
-
if (routeInfo.routeAction !== 'replace' && !shouldPreserveLeavingView) {
|
|
1859
|
-
leavingViewItem.mount = false;
|
|
1860
|
-
}
|
|
1861
|
-
else if (shouldPreserveLeavingView) {
|
|
1862
|
-
this.preservedViewItems.add(leavingViewItem);
|
|
1863
|
-
}
|
|
1864
|
-
this.handleLeavingViewUnmount(routeInfo, enteringViewItem, leavingViewItem);
|
|
1865
|
-
}
|
|
1866
|
-
// Clean up orphaned sibling views after replace actions (redirects)
|
|
1867
|
-
this.cleanupOrphanedSiblingViews(routeInfo, enteringViewItem, leavingViewItem);
|
|
1868
|
-
// On a fresh push, browser forward history is invalidated. Any views we
|
|
1869
|
-
// previously preserved on pop (to support forward navigation) are now
|
|
1870
|
-
// unreachable and should be unmounted so they don't accumulate in the DOM.
|
|
1871
|
-
this.cleanupPreservedViewsOnPush(routeInfo, enteringViewItem, leavingViewItem);
|
|
1872
|
-
}
|
|
1873
|
-
/**
|
|
1874
|
-
* Unmounts views previously kept alive by the pop-preserve logic when a fresh
|
|
1875
|
-
* push invalidates the forward-history path that made them reachable. Only
|
|
1876
|
-
* iterates views explicitly tracked in `preservedViewItems` so that views
|
|
1877
|
-
* naturally mounted through forward-push (the default leaving-view behavior)
|
|
1878
|
-
* are left untouched.
|
|
1879
|
-
*/
|
|
1880
|
-
cleanupPreservedViewsOnPush(routeInfo, enteringViewItem, leavingViewItem) {
|
|
1881
|
-
if (routeInfo.routeAction !== 'push') {
|
|
1882
|
-
return;
|
|
1883
|
-
}
|
|
1884
|
-
if (this.preservedViewItems.size === 0) {
|
|
1885
|
-
return;
|
|
1886
|
-
}
|
|
1887
|
-
for (const viewItem of Array.from(this.preservedViewItems)) {
|
|
1888
|
-
if (viewItem === enteringViewItem || viewItem === leavingViewItem) {
|
|
1889
|
-
this.preservedViewItems.delete(viewItem);
|
|
1890
|
-
continue;
|
|
1891
|
-
}
|
|
1892
|
-
if (!viewItem.mount) {
|
|
1893
|
-
this.preservedViewItems.delete(viewItem);
|
|
1894
|
-
continue;
|
|
8
|
+
class IonRouteInner extends React.PureComponent {
|
|
9
|
+
render() {
|
|
10
|
+
return (React.createElement(Route, Object.assign({ path: this.props.path, exact: this.props.exact, render: this.props.render }, (this.props.computedMatch !== undefined
|
|
11
|
+
? {
|
|
12
|
+
computedMatch: this.props.computedMatch,
|
|
1895
13
|
}
|
|
1896
|
-
|
|
1897
|
-
this.preservedViewItems.delete(viewItem);
|
|
1898
|
-
const viewToUnmount = viewItem;
|
|
1899
|
-
setTimeout(() => {
|
|
1900
|
-
// Skip if a follow-up transition re-entered the view (mount flipped back to true).
|
|
1901
|
-
if (viewToUnmount.mount) {
|
|
1902
|
-
return;
|
|
1903
|
-
}
|
|
1904
|
-
this.context.unMountViewItem(viewToUnmount);
|
|
1905
|
-
this.forceUpdate();
|
|
1906
|
-
}, VIEW_UNMOUNT_DELAY_MS);
|
|
1907
|
-
}
|
|
14
|
+
: {}))));
|
|
1908
15
|
}
|
|
1909
|
-
|
|
1910
|
-
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @see https://v5.reactrouter.com/web/api/matchPath
|
|
20
|
+
*/
|
|
21
|
+
const matchPath = ({ pathname, componentProps, }) => {
|
|
22
|
+
const { exact, component } = componentProps;
|
|
23
|
+
const path = componentProps.path || componentProps.from;
|
|
24
|
+
/***
|
|
25
|
+
* The props to match against, they are identical
|
|
26
|
+
* to the matching props `Route` accepts. It could also be a string
|
|
27
|
+
* or an array of strings as shortcut for `{ path }`.
|
|
1911
28
|
*/
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
const viewToUnmount = leavingViewItem;
|
|
1921
|
-
setTimeout(() => {
|
|
1922
|
-
// Skip if a follow-up transition re-entered the view (mount flipped back to true).
|
|
1923
|
-
if (viewToUnmount.mount) {
|
|
1924
|
-
return;
|
|
1925
|
-
}
|
|
1926
|
-
this.context.unMountViewItem(viewToUnmount);
|
|
1927
|
-
this.forceUpdate();
|
|
1928
|
-
}, VIEW_UNMOUNT_DELAY_MS);
|
|
1929
|
-
return;
|
|
1930
|
-
}
|
|
1931
|
-
const enteringRoutePath = (_b = (_a = enteringViewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path;
|
|
1932
|
-
const leavingRoutePath = (_d = (_c = leavingViewItem.reactElement) === null || _c === void 0 ? void 0 : _c.props) === null || _d === void 0 ? void 0 : _d.path;
|
|
1933
|
-
const isEnteringContainerRoute = enteringRoutePath && enteringRoutePath.endsWith('/*');
|
|
1934
|
-
const isLeavingSpecificRoute = leavingRoutePath &&
|
|
1935
|
-
leavingRoutePath !== '' &&
|
|
1936
|
-
leavingRoutePath !== '*' &&
|
|
1937
|
-
!leavingRoutePath.endsWith('/*') &&
|
|
1938
|
-
!((_f = (_e = leavingViewItem.reactElement) === null || _e === void 0 ? void 0 : _e.props) === null || _f === void 0 ? void 0 : _f.index);
|
|
1939
|
-
// Skip removal for container-to-container transitions (e.g., /tabs/* → /settings/*).
|
|
1940
|
-
// These routes manage their own nested outlets; unmounting would disrupt child views.
|
|
1941
|
-
if (isEnteringContainerRoute && !isLeavingSpecificRoute) {
|
|
1942
|
-
return;
|
|
1943
|
-
}
|
|
1944
|
-
leavingViewItem.mount = false;
|
|
1945
|
-
const viewToUnmount = leavingViewItem;
|
|
1946
|
-
setTimeout(() => {
|
|
1947
|
-
// Skip if a follow-up transition re-entered the view (mount flipped back to true).
|
|
1948
|
-
if (viewToUnmount.mount) {
|
|
1949
|
-
return;
|
|
1950
|
-
}
|
|
1951
|
-
this.context.unMountViewItem(viewToUnmount);
|
|
1952
|
-
this.forceUpdate();
|
|
1953
|
-
}, VIEW_UNMOUNT_DELAY_MS);
|
|
29
|
+
const matchProps = {
|
|
30
|
+
exact,
|
|
31
|
+
path,
|
|
32
|
+
component,
|
|
33
|
+
};
|
|
34
|
+
const match = matchPath$1(pathname, matchProps);
|
|
35
|
+
if (!match) {
|
|
36
|
+
return false;
|
|
1954
37
|
}
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
!leavingViewItem &&
|
|
1976
|
-
((_e = routeInfo.prevRouteLastPathname) === null || _e === void 0 ? void 0 : _e.startsWith(enteringRoutePath.replace(/\/\*$/, '')));
|
|
1977
|
-
if (isSameView || isSameContainerRoute || isNavigatingWithinContainer) {
|
|
1978
|
-
return;
|
|
1979
|
-
}
|
|
1980
|
-
const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id);
|
|
1981
|
-
const areSiblingRoutes = (path1, path2) => {
|
|
1982
|
-
const path1IsRelative = !path1.startsWith('/');
|
|
1983
|
-
const path2IsRelative = !path2.startsWith('/');
|
|
1984
|
-
if (path1IsRelative && path2IsRelative) {
|
|
1985
|
-
const path1Depth = path1.replace(/\/\*$/, '').split('/').filter(Boolean).length;
|
|
1986
|
-
const path2Depth = path2.replace(/\/\*$/, '').split('/').filter(Boolean).length;
|
|
1987
|
-
return path1Depth === path2Depth && path1Depth <= 1;
|
|
1988
|
-
}
|
|
1989
|
-
const getParent = (path) => {
|
|
1990
|
-
const normalized = path.replace(/\/\*$/, '');
|
|
1991
|
-
const segments = normalized.split('/').filter(Boolean);
|
|
1992
|
-
// Strip trailing parameter segments (e.g., :id) so that
|
|
1993
|
-
// sibling routes like /items/list/:id and /items/detail/:id
|
|
1994
|
-
// resolve to the same parent (/items).
|
|
1995
|
-
while (segments.length > 0 && segments[segments.length - 1].startsWith(':')) {
|
|
1996
|
-
segments.pop();
|
|
1997
|
-
}
|
|
1998
|
-
segments.pop();
|
|
1999
|
-
return segments.length > 0 ? '/' + segments.join('/') : '/';
|
|
2000
|
-
};
|
|
2001
|
-
const parent = getParent(path1);
|
|
2002
|
-
// Exclude root-level routes from sibling detection to avoid unintended
|
|
2003
|
-
// cleanup of unrelated top-level routes. Also covers single-depth param
|
|
2004
|
-
// routes (e.g., /items/:id) which resolve to root after param stripping.
|
|
2005
|
-
if (parent === '/') {
|
|
2006
|
-
return false;
|
|
2007
|
-
}
|
|
2008
|
-
return parent === getParent(path2);
|
|
38
|
+
return match;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
class ReactRouterViewStack extends ViewStacks {
|
|
42
|
+
constructor() {
|
|
43
|
+
super();
|
|
44
|
+
this.createViewItem = this.createViewItem.bind(this);
|
|
45
|
+
this.findViewItemByRouteInfo = this.findViewItemByRouteInfo.bind(this);
|
|
46
|
+
this.findLeavingViewItemByRouteInfo = this.findLeavingViewItemByRouteInfo.bind(this);
|
|
47
|
+
this.getChildrenToRender = this.getChildrenToRender.bind(this);
|
|
48
|
+
this.findViewItemByPathname = this.findViewItemByPathname.bind(this);
|
|
49
|
+
}
|
|
50
|
+
createViewItem(outletId, reactElement, routeInfo, page) {
|
|
51
|
+
const viewItem = {
|
|
52
|
+
id: generateId('viewItem'),
|
|
53
|
+
outletId,
|
|
54
|
+
ionPageElement: page,
|
|
55
|
+
reactElement,
|
|
56
|
+
mount: true,
|
|
57
|
+
ionRoute: false,
|
|
2009
58
|
};
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
59
|
+
if (reactElement.type === IonRoute) {
|
|
60
|
+
viewItem.ionRoute = true;
|
|
61
|
+
viewItem.disableIonPageManagement = reactElement.props.disableIonPageManagement;
|
|
62
|
+
}
|
|
63
|
+
viewItem.routeData = {
|
|
64
|
+
match: matchPath({
|
|
65
|
+
pathname: routeInfo.pathname,
|
|
66
|
+
componentProps: reactElement.props,
|
|
67
|
+
}),
|
|
68
|
+
childProps: reactElement.props,
|
|
69
|
+
};
|
|
70
|
+
return viewItem;
|
|
71
|
+
}
|
|
72
|
+
getChildrenToRender(outletId, ionRouterOutlet, routeInfo) {
|
|
73
|
+
const viewItems = this.getViewItemsForOutlet(outletId);
|
|
74
|
+
// Sync latest routes with viewItems
|
|
75
|
+
React.Children.forEach(ionRouterOutlet.props.children, (child) => {
|
|
76
|
+
const viewItem = viewItems.find((v) => {
|
|
77
|
+
return matchComponent$1(child, v.routeData.childProps.path || v.routeData.childProps.from);
|
|
78
|
+
});
|
|
79
|
+
if (viewItem) {
|
|
80
|
+
viewItem.reactElement = child;
|
|
2021
81
|
}
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
let
|
|
2025
|
-
if (
|
|
2026
|
-
|
|
82
|
+
});
|
|
83
|
+
const children = viewItems.map((viewItem) => {
|
|
84
|
+
let clonedChild;
|
|
85
|
+
if (viewItem.ionRoute && !viewItem.disableIonPageManagement) {
|
|
86
|
+
clonedChild = (React.createElement(ViewLifeCycleManager, { key: `view-${viewItem.id}`, mount: viewItem.mount, removeView: () => this.remove(viewItem) }, React.cloneElement(viewItem.reactElement, {
|
|
87
|
+
computedMatch: viewItem.routeData.match,
|
|
88
|
+
})));
|
|
2027
89
|
}
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
viewItem.mount
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
this.context.unMountViewItem(viewToRemove);
|
|
2038
|
-
this.forceUpdate();
|
|
2039
|
-
}, VIEW_UNMOUNT_DELAY_MS);
|
|
90
|
+
else {
|
|
91
|
+
const match = matchComponent$1(viewItem.reactElement, routeInfo.pathname);
|
|
92
|
+
clonedChild = (React.createElement(ViewLifeCycleManager, { key: `view-${viewItem.id}`, mount: viewItem.mount, removeView: () => this.remove(viewItem) }, React.cloneElement(viewItem.reactElement, {
|
|
93
|
+
computedMatch: viewItem.routeData.match,
|
|
94
|
+
})));
|
|
95
|
+
if (!match && viewItem.routeData.match) {
|
|
96
|
+
viewItem.routeData.match = undefined;
|
|
97
|
+
viewItem.mount = false;
|
|
98
|
+
}
|
|
2040
99
|
}
|
|
2041
|
-
|
|
100
|
+
return clonedChild;
|
|
101
|
+
});
|
|
102
|
+
return children;
|
|
2042
103
|
}
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
* area (i.e., an ion-content sits between the outlet and the .ion-page). These
|
|
2049
|
-
* outlets render child pages inside a parent page's scrollable area, and the MD
|
|
2050
|
-
* animation shows both entering and leaving pages simultaneously — causing text
|
|
2051
|
-
* overlap and nested scrollbars. Standard page-level outlets (tabs, routing,
|
|
2052
|
-
* swipe-to-go-back) animate normally even though they sit inside a framework-
|
|
2053
|
-
* managed .ion-page wrapper from the parent outlet's view stack.
|
|
2054
|
-
*
|
|
2055
|
-
* Uses inline visibility:hidden rather than ion-page-hidden class because
|
|
2056
|
-
* core's beforeTransition() removes ion-page-hidden via setPageHidden().
|
|
2057
|
-
* Inline visibility:hidden survives that removal, keeping the page hidden
|
|
2058
|
-
* until React unmounts it after ionViewDidLeave fires. Unlike display:none,
|
|
2059
|
-
* visibility:hidden preserves element geometry so commit() animations
|
|
2060
|
-
* can resolve normally.
|
|
2061
|
-
*/
|
|
2062
|
-
applySkipAnimationIfNeeded(enteringViewItem, leavingViewItem) {
|
|
2063
|
-
var _a, _b;
|
|
2064
|
-
// Only skip for outlets genuinely nested inside a page's content area.
|
|
2065
|
-
// Walk from the outlet up to the nearest .ion-page; if an ion-content
|
|
2066
|
-
// sits in between, the outlet is inside scrollable page content and
|
|
2067
|
-
// animating would cause overlapping pages with duplicate scrollbars.
|
|
2068
|
-
let isInsidePageContent = false;
|
|
2069
|
-
let el = (_b = (_a = this.routerOutletElement) === null || _a === void 0 ? void 0 : _a.parentElement) !== null && _b !== void 0 ? _b : null;
|
|
2070
|
-
while (el) {
|
|
2071
|
-
if (el.classList.contains('ion-page'))
|
|
2072
|
-
break;
|
|
2073
|
-
if (el.tagName === 'ION-CONTENT') {
|
|
2074
|
-
isInsidePageContent = true;
|
|
2075
|
-
break;
|
|
2076
|
-
}
|
|
2077
|
-
el = el.parentElement;
|
|
104
|
+
findViewItemByRouteInfo(routeInfo, outletId, updateMatch) {
|
|
105
|
+
const { viewItem, match } = this.findViewItemByPath(routeInfo.pathname, outletId);
|
|
106
|
+
const shouldUpdateMatch = updateMatch === undefined || updateMatch === true;
|
|
107
|
+
if (shouldUpdateMatch && viewItem && match) {
|
|
108
|
+
viewItem.routeData.match = match;
|
|
2078
109
|
}
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
110
|
+
return viewItem;
|
|
111
|
+
}
|
|
112
|
+
findLeavingViewItemByRouteInfo(routeInfo, outletId, mustBeIonRoute = true) {
|
|
113
|
+
const { viewItem } = this.findViewItemByPath(routeInfo.lastPathname, outletId, mustBeIonRoute);
|
|
114
|
+
return viewItem;
|
|
115
|
+
}
|
|
116
|
+
findViewItemByPathname(pathname, outletId) {
|
|
117
|
+
const { viewItem } = this.findViewItemByPath(pathname, outletId);
|
|
118
|
+
return viewItem;
|
|
2085
119
|
}
|
|
2086
120
|
/**
|
|
2087
|
-
*
|
|
121
|
+
* Returns the matching view item and the match result for a given pathname.
|
|
2088
122
|
*/
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
if (
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
}
|
|
2099
|
-
this.pendingPageTransition = false;
|
|
2100
|
-
// Hide ALL other visible views in this outlet for Navigate redirects.
|
|
2101
|
-
// Same rationale as the timeout path: intermediate redirects can shift
|
|
2102
|
-
// the leaving view reference, leaving the original page visible.
|
|
2103
|
-
const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id);
|
|
2104
|
-
allViewsInOutlet.forEach((viewItem) => {
|
|
2105
|
-
if (viewItem.id !== enteringViewItem.id && viewItem.ionPageElement) {
|
|
2106
|
-
hideIonPageElement(viewItem.ionPageElement);
|
|
2107
|
-
}
|
|
2108
|
-
});
|
|
2109
|
-
// Don't unmount if entering and leaving are the same view item
|
|
2110
|
-
if (shouldUnmountLeavingViewItem && leavingViewItem && enteringViewItem !== leavingViewItem) {
|
|
2111
|
-
const shouldPreserveLeavingView = routeInfo.routeAction === 'pop' && isViewItemPreservableOnPop(leavingViewItem);
|
|
2112
|
-
if (routeInfo.routeAction !== 'replace' && !shouldPreserveLeavingView) {
|
|
2113
|
-
leavingViewItem.mount = false;
|
|
2114
|
-
}
|
|
2115
|
-
else if (shouldPreserveLeavingView) {
|
|
2116
|
-
this.preservedViewItems.add(leavingViewItem);
|
|
2117
|
-
}
|
|
2118
|
-
this.handleLeavingViewUnmount(routeInfo, enteringViewItem, leavingViewItem);
|
|
123
|
+
findViewItemByPath(pathname, outletId, mustBeIonRoute) {
|
|
124
|
+
let viewItem;
|
|
125
|
+
let match;
|
|
126
|
+
let viewStack;
|
|
127
|
+
if (outletId) {
|
|
128
|
+
viewStack = this.getViewItemsForOutlet(outletId);
|
|
129
|
+
viewStack.some(matchView);
|
|
130
|
+
if (!viewItem) {
|
|
131
|
+
viewStack.some(matchDefaultRoute);
|
|
2119
132
|
}
|
|
2120
|
-
this.cleanupPreservedViewsOnPush(routeInfo, enteringViewItem, leavingViewItem);
|
|
2121
|
-
this.forceUpdate();
|
|
2122
|
-
return;
|
|
2123
133
|
}
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
clearTimeout(this.ionPageWaitTimeout);
|
|
134
|
+
else {
|
|
135
|
+
const viewItems = this.getAllViewItems();
|
|
136
|
+
viewItems.some(matchView);
|
|
137
|
+
if (!viewItem) {
|
|
138
|
+
viewItems.some(matchDefaultRoute);
|
|
139
|
+
}
|
|
2131
140
|
}
|
|
2132
|
-
|
|
141
|
+
return { viewItem, match };
|
|
142
|
+
function matchView(v) {
|
|
2133
143
|
var _a, _b;
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
return;
|
|
2137
|
-
}
|
|
2138
|
-
this.waitingForIonPage = false;
|
|
2139
|
-
const latestEnteringView = (_a = this.context.findViewItemByRouteInfo(routeInfo, this.id)) !== null && _a !== void 0 ? _a : enteringViewItem;
|
|
2140
|
-
const latestLeavingView = (_b = this.context.findLeavingViewItemByRouteInfo(routeInfo, this.id)) !== null && _b !== void 0 ? _b : leavingViewItem;
|
|
2141
|
-
if (latestEnteringView === null || latestEnteringView === void 0 ? void 0 : latestEnteringView.ionPageElement) {
|
|
2142
|
-
const shouldSkipAnimation = this.applySkipAnimationIfNeeded(latestEnteringView, latestLeavingView !== null && latestLeavingView !== void 0 ? latestLeavingView : undefined);
|
|
2143
|
-
this.transitionPage(routeInfo, latestEnteringView, latestLeavingView !== null && latestLeavingView !== void 0 ? latestLeavingView : undefined, undefined, false, shouldSkipAnimation);
|
|
2144
|
-
if (shouldUnmountLeavingViewItem && latestLeavingView && latestEnteringView !== latestLeavingView) {
|
|
2145
|
-
const shouldPreserveLeavingView = routeInfo.routeAction === 'pop' && isViewItemPreservableOnPop(latestLeavingView);
|
|
2146
|
-
if (routeInfo.routeAction !== 'replace' && !shouldPreserveLeavingView) {
|
|
2147
|
-
latestLeavingView.mount = false;
|
|
2148
|
-
}
|
|
2149
|
-
else if (shouldPreserveLeavingView) {
|
|
2150
|
-
this.preservedViewItems.add(latestLeavingView);
|
|
2151
|
-
}
|
|
2152
|
-
this.handleLeavingViewUnmount(routeInfo, latestEnteringView, latestLeavingView);
|
|
2153
|
-
}
|
|
2154
|
-
this.cleanupPreservedViewsOnPush(routeInfo, latestEnteringView, latestLeavingView !== null && latestLeavingView !== void 0 ? latestLeavingView : undefined);
|
|
2155
|
-
this.forceUpdate();
|
|
144
|
+
if (mustBeIonRoute && !v.ionRoute) {
|
|
145
|
+
return false;
|
|
2156
146
|
}
|
|
2157
|
-
|
|
147
|
+
match = matchPath({
|
|
148
|
+
pathname,
|
|
149
|
+
componentProps: v.routeData.childProps,
|
|
150
|
+
});
|
|
151
|
+
if (match) {
|
|
2158
152
|
/**
|
|
2159
|
-
*
|
|
2160
|
-
*
|
|
2161
|
-
*
|
|
2162
|
-
*
|
|
2163
|
-
* change the leaving view reference, leaving the original page still visible.
|
|
153
|
+
* Even though we have a match from react-router, we do not know if the match
|
|
154
|
+
* is for this specific view item.
|
|
155
|
+
*
|
|
156
|
+
* To validate this, we need to check if the path and url match the view item's route data.
|
|
2164
157
|
*/
|
|
2165
|
-
const
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
});
|
|
2185
|
-
}, ION_PAGE_WAIT_TIMEOUT_MS);
|
|
158
|
+
const hasParameter = match.path.includes(':');
|
|
159
|
+
if (!hasParameter || (hasParameter && match.url === ((_b = (_a = v.routeData) === null || _a === void 0 ? void 0 : _a.match) === null || _b === void 0 ? void 0 : _b.url))) {
|
|
160
|
+
viewItem = v;
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
function matchDefaultRoute(v) {
|
|
167
|
+
// try to find a route that doesn't have a path or from prop, that will be our default route
|
|
168
|
+
if (!v.routeData.childProps.path && !v.routeData.childProps.from) {
|
|
169
|
+
match = {
|
|
170
|
+
path: pathname,
|
|
171
|
+
url: pathname,
|
|
172
|
+
isExact: true,
|
|
173
|
+
params: {},
|
|
174
|
+
};
|
|
175
|
+
viewItem = v;
|
|
176
|
+
return true;
|
|
2186
177
|
}
|
|
2187
|
-
|
|
2188
|
-
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
2189
180
|
}
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
181
|
+
}
|
|
182
|
+
function matchComponent$1(node, pathname) {
|
|
183
|
+
return matchPath({
|
|
184
|
+
pathname,
|
|
185
|
+
componentProps: node.props,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function clonePageElement(leavingViewHtml) {
|
|
190
|
+
let html;
|
|
191
|
+
if (typeof leavingViewHtml === 'string') {
|
|
192
|
+
html = leavingViewHtml;
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
html = leavingViewHtml.outerHTML;
|
|
196
|
+
}
|
|
197
|
+
if (document) {
|
|
198
|
+
const newEl = document.createElement('div');
|
|
199
|
+
newEl.innerHTML = html;
|
|
200
|
+
newEl.style.zIndex = '';
|
|
201
|
+
// Remove an existing back button so the new element doesn't get two of them
|
|
202
|
+
const ionBackButton = newEl.getElementsByTagName('ion-back-button');
|
|
203
|
+
if (ionBackButton[0]) {
|
|
204
|
+
ionBackButton[0].remove();
|
|
205
|
+
}
|
|
206
|
+
return newEl.firstChild;
|
|
207
|
+
}
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const isViewVisible = (el) => !el.classList.contains('ion-page-invisible') && !el.classList.contains('ion-page-hidden');
|
|
212
|
+
class StackManager extends React.PureComponent {
|
|
213
|
+
constructor(props) {
|
|
214
|
+
super(props);
|
|
215
|
+
this.stackContextValue = {
|
|
216
|
+
registerIonPage: this.registerIonPage.bind(this),
|
|
217
|
+
isInOutlet: () => true,
|
|
218
|
+
};
|
|
219
|
+
this.pendingPageTransition = false;
|
|
220
|
+
this.registerIonPage = this.registerIonPage.bind(this);
|
|
221
|
+
this.transitionPage = this.transitionPage.bind(this);
|
|
222
|
+
this.handlePageTransition = this.handlePageTransition.bind(this);
|
|
223
|
+
this.id = generateId('routerOutlet');
|
|
224
|
+
this.prevProps = undefined;
|
|
225
|
+
this.skipTransition = false;
|
|
2199
226
|
}
|
|
2200
227
|
componentDidMount() {
|
|
2201
|
-
this.
|
|
228
|
+
if (this.clearOutletTimeout) {
|
|
229
|
+
/**
|
|
230
|
+
* The clearOutlet integration with React Router is a bit hacky.
|
|
231
|
+
* It uses a timeout to clear the outlet after a transition.
|
|
232
|
+
* In React v18, components are mounted and unmounted in development mode
|
|
233
|
+
* to check for side effects.
|
|
234
|
+
*
|
|
235
|
+
* This clearTimeout prevents the outlet from being cleared when the component is re-mounted,
|
|
236
|
+
* which should only happen in development mode and as a result of a hot reload.
|
|
237
|
+
*/
|
|
238
|
+
clearTimeout(this.clearOutletTimeout);
|
|
239
|
+
}
|
|
2202
240
|
if (this.routerOutletElement) {
|
|
2203
241
|
this.setupRouterOutlet(this.routerOutletElement);
|
|
2204
|
-
|
|
2205
|
-
// React 19's reappearLayoutEffects phase, which re-runs componentDidMount
|
|
2206
|
-
// without a preceding componentWillUnmount and causes "Maximum update depth exceeded".
|
|
2207
|
-
const routeInfo = this.props.routeInfo;
|
|
2208
|
-
queueMicrotask(() => {
|
|
2209
|
-
if (this._isMounted && this.props.routeInfo.pathname === routeInfo.pathname) {
|
|
2210
|
-
this.handlePageTransition(routeInfo);
|
|
2211
|
-
}
|
|
2212
|
-
});
|
|
242
|
+
this.handlePageTransition(this.props.routeInfo);
|
|
2213
243
|
}
|
|
2214
244
|
}
|
|
2215
245
|
componentDidUpdate(prevProps) {
|
|
@@ -2225,197 +255,117 @@ class StackManager extends React.PureComponent {
|
|
|
2225
255
|
}
|
|
2226
256
|
}
|
|
2227
257
|
componentWillUnmount() {
|
|
2228
|
-
this.
|
|
2229
|
-
// Cancel any in-flight transition rAFs
|
|
2230
|
-
for (const id of this.transitionRafIds) {
|
|
2231
|
-
cancelAnimationFrame(id);
|
|
2232
|
-
}
|
|
2233
|
-
this.transitionRafIds = [];
|
|
2234
|
-
// Disconnect any in-flight MutationObserver from waitForComponentsReady
|
|
2235
|
-
if (this.transitionObserver) {
|
|
2236
|
-
this.transitionObserver.disconnect();
|
|
2237
|
-
this.transitionObserver = undefined;
|
|
2238
|
-
}
|
|
2239
|
-
if (this.ionPageWaitTimeout) {
|
|
2240
|
-
clearTimeout(this.ionPageWaitTimeout);
|
|
2241
|
-
this.ionPageWaitTimeout = undefined;
|
|
2242
|
-
}
|
|
2243
|
-
if (this.outOfScopeUnmountTimeout) {
|
|
2244
|
-
clearTimeout(this.outOfScopeUnmountTimeout);
|
|
2245
|
-
this.outOfScopeUnmountTimeout = undefined;
|
|
2246
|
-
}
|
|
2247
|
-
this.waitingForIonPage = false;
|
|
2248
|
-
this.preservedViewItems.clear();
|
|
2249
|
-
// Hide all views in this outlet before clearing.
|
|
2250
|
-
// This is critical for nested outlets - when the parent component unmounts,
|
|
2251
|
-
// the nested outlet's componentDidUpdate won't be called, so we must hide
|
|
2252
|
-
// the ion-page elements here to prevent them from remaining visible on top
|
|
2253
|
-
// of other content after navigation to a different route.
|
|
2254
|
-
const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id);
|
|
2255
|
-
allViewsInOutlet.forEach((viewItem) => {
|
|
2256
|
-
hideIonPageElement(viewItem.ionPageElement);
|
|
2257
|
-
});
|
|
2258
|
-
this.context.clearOutlet(this.id);
|
|
258
|
+
this.clearOutletTimeout = this.context.clearOutlet(this.id);
|
|
2259
259
|
}
|
|
2260
|
-
/**
|
|
2261
|
-
* Sets the transition between pages within this router outlet.
|
|
2262
|
-
* This function determines the entering and leaving views based on the
|
|
2263
|
-
* provided route information and triggers the appropriate animation.
|
|
2264
|
-
* It also handles scenarios like initial loads, back navigation, and
|
|
2265
|
-
* navigation to the same view with different parameters.
|
|
2266
|
-
*
|
|
2267
|
-
* @param routeInfo It contains info about the current route,
|
|
2268
|
-
* the previous route, and the action taken (e.g., push, replace).
|
|
2269
|
-
*
|
|
2270
|
-
* @returns A promise that resolves when the transition is complete.
|
|
2271
|
-
* If no transition is needed or if the router outlet isn't ready,
|
|
2272
|
-
* the Promise may resolve immediately.
|
|
2273
|
-
*/
|
|
2274
260
|
async handlePageTransition(routeInfo) {
|
|
2275
|
-
var _a;
|
|
2276
|
-
// Wait for router outlet to mount
|
|
261
|
+
var _a, _b;
|
|
2277
262
|
if (!this.routerOutletElement || !this.routerOutletElement.commit) {
|
|
263
|
+
/**
|
|
264
|
+
* The route outlet has not mounted yet. We need to wait for it to render
|
|
265
|
+
* before we can transition the page.
|
|
266
|
+
*
|
|
267
|
+
* Set a flag to indicate that we should transition the page after
|
|
268
|
+
* the component has updated.
|
|
269
|
+
*/
|
|
2278
270
|
this.pendingPageTransition = true;
|
|
2279
|
-
return;
|
|
2280
|
-
}
|
|
2281
|
-
// Find entering and leaving view items
|
|
2282
|
-
const viewItems = this.findViewItems(routeInfo);
|
|
2283
|
-
let enteringViewItem = viewItems.enteringViewItem;
|
|
2284
|
-
let leavingViewItem = viewItems.leavingViewItem;
|
|
2285
|
-
let shouldUnmountLeavingViewItem = this.shouldUnmountLeavingView(routeInfo, enteringViewItem, leavingViewItem);
|
|
2286
|
-
// Get parent path for nested outlets
|
|
2287
|
-
const parentPath = this.getParentPath();
|
|
2288
|
-
// Handle out-of-scope outlet (route outside mount path)
|
|
2289
|
-
if (this.handleOutOfScopeOutlet(routeInfo)) {
|
|
2290
|
-
return;
|
|
2291
|
-
}
|
|
2292
|
-
// Handle root navigation: unmount all non-entering views
|
|
2293
|
-
if (routeInfo.routeDirection === 'root') {
|
|
2294
|
-
this.handleRootNavigation(enteringViewItem);
|
|
2295
|
-
leavingViewItem = undefined;
|
|
2296
|
-
shouldUnmountLeavingViewItem = false;
|
|
2297
271
|
}
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
// Handle nested outlet with relative routes but no valid parent path
|
|
2304
|
-
if (this.handleOutOfContextNestedOutlet(parentPath, leavingViewItem)) {
|
|
2305
|
-
return;
|
|
2306
|
-
}
|
|
2307
|
-
// Find the matching route element
|
|
2308
|
-
const enteringRoute = findRouteByRouteInfo((_a = this.ionRouterOutlet) === null || _a === void 0 ? void 0 : _a.props.children, routeInfo, parentPath);
|
|
2309
|
-
// Handle nested outlet with no matching route
|
|
2310
|
-
if (this.handleNoMatchingRoute(enteringRoute, enteringViewItem, leavingViewItem)) {
|
|
2311
|
-
return;
|
|
2312
|
-
}
|
|
2313
|
-
// Create or update the entering view item
|
|
2314
|
-
if (enteringViewItem && enteringRoute) {
|
|
2315
|
-
enteringViewItem.reactElement = enteringRoute;
|
|
2316
|
-
}
|
|
2317
|
-
else if (enteringRoute) {
|
|
2318
|
-
enteringViewItem = this.context.createViewItem(this.id, enteringRoute, routeInfo);
|
|
2319
|
-
this.context.addViewItem(enteringViewItem);
|
|
2320
|
-
}
|
|
2321
|
-
// Handle transition based on ion-page element availability
|
|
2322
|
-
// Check if the ionPageElement is still in the document.
|
|
2323
|
-
// If the view was previously unmounted (mount=false), the ViewLifeCycleManager
|
|
2324
|
-
// removes the React component from the tree, which removes the IonPage from the DOM.
|
|
2325
|
-
// The ionPageElement reference becomes stale and we need to wait for a new one.
|
|
2326
|
-
const ionPageIsInDocument = (enteringViewItem === null || enteringViewItem === void 0 ? void 0 : enteringViewItem.ionPageElement) && document.body.contains(enteringViewItem.ionPageElement);
|
|
2327
|
-
if (enteringViewItem && ionPageIsInDocument) {
|
|
2328
|
-
// Clear waiting state
|
|
2329
|
-
if (this.waitingForIonPage) {
|
|
2330
|
-
this.waitingForIonPage = false;
|
|
272
|
+
else {
|
|
273
|
+
let enteringViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id);
|
|
274
|
+
let leavingViewItem = this.context.findLeavingViewItemByRouteInfo(routeInfo, this.id);
|
|
275
|
+
if (!leavingViewItem && routeInfo.prevRouteLastPathname) {
|
|
276
|
+
leavingViewItem = this.context.findViewItemByPathname(routeInfo.prevRouteLastPathname, this.id);
|
|
2331
277
|
}
|
|
2332
|
-
if
|
|
2333
|
-
|
|
2334
|
-
|
|
278
|
+
// Check if leavingViewItem should be unmounted
|
|
279
|
+
if (leavingViewItem) {
|
|
280
|
+
if (routeInfo.routeAction === 'replace') {
|
|
281
|
+
leavingViewItem.mount = false;
|
|
282
|
+
}
|
|
283
|
+
else if (!(routeInfo.routeAction === 'push' && routeInfo.routeDirection === 'forward')) {
|
|
284
|
+
if (routeInfo.routeDirection !== 'none' && enteringViewItem !== leavingViewItem) {
|
|
285
|
+
leavingViewItem.mount = false;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
else if ((_a = routeInfo.routeOptions) === null || _a === void 0 ? void 0 : _a.unmount) {
|
|
289
|
+
leavingViewItem.mount = false;
|
|
290
|
+
}
|
|
2335
291
|
}
|
|
2336
|
-
this.
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
// Wait for ion-page to mount
|
|
2340
|
-
// This handles both: no ionPageElement, or stale ionPageElement (not in document)
|
|
2341
|
-
// Clear stale reference if the element is no longer in the document
|
|
2342
|
-
if (enteringViewItem.ionPageElement && !document.body.contains(enteringViewItem.ionPageElement)) {
|
|
2343
|
-
enteringViewItem.ionPageElement = undefined;
|
|
292
|
+
const enteringRoute = matchRoute((_b = this.ionRouterOutlet) === null || _b === void 0 ? void 0 : _b.props.children, routeInfo);
|
|
293
|
+
if (enteringViewItem) {
|
|
294
|
+
enteringViewItem.reactElement = enteringRoute;
|
|
2344
295
|
}
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
enteringViewItem
|
|
296
|
+
else if (enteringRoute) {
|
|
297
|
+
enteringViewItem = this.context.createViewItem(this.id, enteringRoute, routeInfo);
|
|
298
|
+
this.context.addViewItem(enteringViewItem);
|
|
2348
299
|
}
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
300
|
+
if (enteringViewItem && enteringViewItem.ionPageElement) {
|
|
301
|
+
/**
|
|
302
|
+
* If the entering view item is the same as the leaving view item,
|
|
303
|
+
* then we don't need to transition.
|
|
304
|
+
*/
|
|
305
|
+
if (enteringViewItem === leavingViewItem) {
|
|
306
|
+
/**
|
|
307
|
+
* If the entering view item is the same as the leaving view item,
|
|
308
|
+
* we are either transitioning using parameterized routes to the same view
|
|
309
|
+
* or a parent router outlet is re-rendering as a result of React props changing.
|
|
310
|
+
*
|
|
311
|
+
* If the route data does not match the current path, the parent router outlet
|
|
312
|
+
* is attempting to transition and we cancel the operation.
|
|
313
|
+
*/
|
|
314
|
+
if (enteringViewItem.routeData.match.url !== routeInfo.pathname) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* If there isn't a leaving view item, but the route info indicates
|
|
320
|
+
* that the user has routed from a previous path, then we need
|
|
321
|
+
* to find the leaving view item to transition between.
|
|
322
|
+
*/
|
|
323
|
+
if (!leavingViewItem && this.props.routeInfo.prevRouteLastPathname) {
|
|
324
|
+
leavingViewItem = this.context.findViewItemByPathname(this.props.routeInfo.prevRouteLastPathname, this.id);
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* If the entering view is already visible and the leaving view is not, the transition does not need to occur.
|
|
328
|
+
*/
|
|
329
|
+
if (isViewVisible(enteringViewItem.ionPageElement) &&
|
|
330
|
+
leavingViewItem !== undefined &&
|
|
331
|
+
!isViewVisible(leavingViewItem.ionPageElement)) {
|
|
332
|
+
return;
|
|
2359
333
|
}
|
|
2360
|
-
|
|
2361
|
-
|
|
334
|
+
/**
|
|
335
|
+
* The view should only be transitioned in the following cases:
|
|
336
|
+
* 1. Performing a replace or pop action, such as a swipe to go back gesture
|
|
337
|
+
* to animation the leaving view off the screen.
|
|
338
|
+
*
|
|
339
|
+
* 2. Navigating between top-level router outlets, such as /page-1 to /page-2;
|
|
340
|
+
* or navigating within a nested outlet, such as /tabs/tab-1 to /tabs/tab-2.
|
|
341
|
+
*
|
|
342
|
+
* 3. The entering view is an ion-router-outlet containing a page
|
|
343
|
+
* matching the current route and that hasn't already transitioned in.
|
|
344
|
+
*
|
|
345
|
+
* This should only happen when navigating directly to a nested router outlet
|
|
346
|
+
* route or on an initial page load (i.e. refreshing). In cases when loading
|
|
347
|
+
* /tabs/tab-1, we need to transition the /tabs page element into the view.
|
|
348
|
+
*/
|
|
349
|
+
this.transitionPage(routeInfo, enteringViewItem, leavingViewItem);
|
|
350
|
+
}
|
|
351
|
+
else if (leavingViewItem && !enteringRoute && !enteringViewItem) {
|
|
352
|
+
// If we have a leavingView but no entering view/route, we are probably leaving to
|
|
353
|
+
// another outlet, so hide this leavingView. We do it in a timeout to give time for a
|
|
354
|
+
// transition to finish.
|
|
355
|
+
// setTimeout(() => {
|
|
356
|
+
if (leavingViewItem.ionPageElement) {
|
|
357
|
+
leavingViewItem.ionPageElement.classList.add('ion-page-hidden');
|
|
358
|
+
leavingViewItem.ionPageElement.setAttribute('aria-hidden', 'true');
|
|
2362
359
|
}
|
|
360
|
+
// }, 250);
|
|
2363
361
|
}
|
|
362
|
+
this.forceUpdate();
|
|
2364
363
|
}
|
|
2365
|
-
this.forceUpdate();
|
|
2366
364
|
}
|
|
2367
|
-
/**
|
|
2368
|
-
* Registers an `<IonPage>` DOM element with the `StackManager`.
|
|
2369
|
-
* This is called when `<IonPage>` has been mounted.
|
|
2370
|
-
*
|
|
2371
|
-
* @param page The element of the rendered `<IonPage>`.
|
|
2372
|
-
* @param routeInfo The route information that associates with `<IonPage>`.
|
|
2373
|
-
*/
|
|
2374
365
|
registerIonPage(page, routeInfo) {
|
|
2375
|
-
/**
|
|
2376
|
-
* DO NOT remove ion-page-invisible here.
|
|
2377
|
-
*
|
|
2378
|
-
* PageManager's ref callback adds ion-page-invisible synchronously to prevent flash.
|
|
2379
|
-
* At this point, the <IonPage> div exists but its CHILDREN (header, toolbar, menu-button)
|
|
2380
|
-
* have NOT rendered yet. If we remove ion-page-invisible now, the page becomes visible
|
|
2381
|
-
* with empty/incomplete content, causing a flicker (especially for ion-menu-button which
|
|
2382
|
-
* starts with menu-button-hidden class).
|
|
2383
|
-
*
|
|
2384
|
-
* Instead, let transitionPage handle visibility AFTER waiting for components to be ready.
|
|
2385
|
-
* This ensures the page only becomes visible when its content is fully rendered.
|
|
2386
|
-
*/
|
|
2387
|
-
this.waitingForIonPage = false;
|
|
2388
|
-
if (this.ionPageWaitTimeout) {
|
|
2389
|
-
clearTimeout(this.ionPageWaitTimeout);
|
|
2390
|
-
this.ionPageWaitTimeout = undefined;
|
|
2391
|
-
}
|
|
2392
|
-
this.pendingPageTransition = false;
|
|
2393
366
|
const foundView = this.context.findViewItemByRouteInfo(routeInfo, this.id);
|
|
2394
367
|
if (foundView) {
|
|
2395
368
|
const oldPageElement = foundView.ionPageElement;
|
|
2396
|
-
/**
|
|
2397
|
-
* FIX for issue #28878: Reject orphaned IonPage registrations.
|
|
2398
|
-
*
|
|
2399
|
-
* When a component conditionally renders different IonPages (e.g., list vs empty state)
|
|
2400
|
-
* using React keys, and state changes simultaneously with navigation, the new IonPage
|
|
2401
|
-
* tries to register for a route we're navigating away from. This creates a stale view.
|
|
2402
|
-
*
|
|
2403
|
-
* Only reject if both pageIds exist and differ, to allow nested outlet registrations.
|
|
2404
|
-
*/
|
|
2405
|
-
if (this.shouldRejectOrphanedPage(page, oldPageElement, routeInfo)) {
|
|
2406
|
-
this.hideAndRemoveOrphanedPage(page);
|
|
2407
|
-
return;
|
|
2408
|
-
}
|
|
2409
|
-
/**
|
|
2410
|
-
* Don't let a nested element (e.g., ion-router-outlet with ionPage prop)
|
|
2411
|
-
* override an existing IonPage registration when the existing element is
|
|
2412
|
-
* an ancestor of the new one. This ensures ionPageElement always points
|
|
2413
|
-
* to the outermost IonPage, which is needed to properly hide the entire
|
|
2414
|
-
* page during back navigation (not just the inner outlet).
|
|
2415
|
-
*/
|
|
2416
|
-
if (oldPageElement && oldPageElement !== page && oldPageElement.isConnected && oldPageElement.contains(page)) {
|
|
2417
|
-
return;
|
|
2418
|
-
}
|
|
2419
369
|
foundView.ionPageElement = page;
|
|
2420
370
|
foundView.ionRoute = true;
|
|
2421
371
|
/**
|
|
@@ -2429,162 +379,83 @@ class StackManager extends React.PureComponent {
|
|
|
2429
379
|
}
|
|
2430
380
|
this.handlePageTransition(routeInfo);
|
|
2431
381
|
}
|
|
2432
|
-
/**
|
|
2433
|
-
* Checks if a new IonPage should be rejected (component re-rendered while navigating away).
|
|
2434
|
-
*/
|
|
2435
|
-
shouldRejectOrphanedPage(newPage, oldPageElement, routeInfo) {
|
|
2436
|
-
if (!oldPageElement || oldPageElement === newPage) {
|
|
2437
|
-
return false;
|
|
2438
|
-
}
|
|
2439
|
-
const newPageId = newPage.getAttribute('data-pageid');
|
|
2440
|
-
const oldPageId = oldPageElement.getAttribute('data-pageid');
|
|
2441
|
-
if (!newPageId || !oldPageId || newPageId === oldPageId) {
|
|
2442
|
-
return false;
|
|
2443
|
-
}
|
|
2444
|
-
return this.props.routeInfo.pathname !== routeInfo.pathname;
|
|
2445
|
-
}
|
|
2446
|
-
hideAndRemoveOrphanedPage(page) {
|
|
2447
|
-
page.classList.add('ion-page-hidden');
|
|
2448
|
-
page.setAttribute('aria-hidden', 'true');
|
|
2449
|
-
setTimeout(() => {
|
|
2450
|
-
if (page.parentElement) {
|
|
2451
|
-
page.remove();
|
|
2452
|
-
}
|
|
2453
|
-
}, VIEW_UNMOUNT_DELAY_MS);
|
|
2454
|
-
}
|
|
2455
|
-
/**
|
|
2456
|
-
* Dismisses every presented Ionic overlay in the document. Core moves overlays
|
|
2457
|
-
* to ion-app when presented, so they are no longer descendants of this outlet
|
|
2458
|
-
* and stay visible even after the outlet is hidden, blocking the entering view.
|
|
2459
|
-
*
|
|
2460
|
-
* Scope is document-wide because the original outlet-to-overlay DOM linkage is
|
|
2461
|
-
* lost after presentation. Overlays without a trackable presenting element
|
|
2462
|
-
* cannot be safely attributed to a specific outlet.
|
|
2463
|
-
*/
|
|
2464
|
-
dismissPresentedOverlays() {
|
|
2465
|
-
// Matches the overlay set tracked by core's getPresentedOverlays (see
|
|
2466
|
-
// core/src/utils/overlays.ts). An overlay is "presented" when it lacks the
|
|
2467
|
-
// `overlay-hidden` class.
|
|
2468
|
-
const overlaySelector = 'ion-modal, ion-popover, ion-action-sheet, ion-alert, ion-loading';
|
|
2469
|
-
document.querySelectorAll(overlaySelector).forEach((overlay) => {
|
|
2470
|
-
if (overlay.classList.contains('overlay-hidden') || typeof overlay.dismiss !== 'function') {
|
|
2471
|
-
return;
|
|
2472
|
-
}
|
|
2473
|
-
overlay.dismiss().catch(() => {
|
|
2474
|
-
/* Overlay may already be dismissing or its canDismiss guard may block it. */
|
|
2475
|
-
});
|
|
2476
|
-
});
|
|
2477
|
-
}
|
|
2478
|
-
/**
|
|
2479
|
-
* Resolves the entering view for a swipe-back gesture.
|
|
2480
|
-
*
|
|
2481
|
-
* Prefers a view owned by this outlet. Falls back to searching all outlets only
|
|
2482
|
-
* when the candidate's ion-page element is a descendant of this outlet. Without
|
|
2483
|
-
* the containment guard, a nested child outlet can claim ownership of a sibling
|
|
2484
|
-
* outlet's view, running the swipe gesture on the wrong router outlet.
|
|
2485
|
-
*/
|
|
2486
|
-
findEnteringViewForSwipe(swipeBackRouteInfo) {
|
|
2487
|
-
var _a;
|
|
2488
|
-
const enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, this.id, false);
|
|
2489
|
-
if (enteringViewItem) {
|
|
2490
|
-
return enteringViewItem;
|
|
2491
|
-
}
|
|
2492
|
-
const candidate = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, undefined, false);
|
|
2493
|
-
if ((candidate === null || candidate === void 0 ? void 0 : candidate.ionPageElement) && ((_a = this.routerOutletElement) === null || _a === void 0 ? void 0 : _a.contains(candidate.ionPageElement))) {
|
|
2494
|
-
return candidate;
|
|
2495
|
-
}
|
|
2496
|
-
return undefined;
|
|
2497
|
-
}
|
|
2498
|
-
/**
|
|
2499
|
-
* Configures swipe-to-go-back gesture for the router outlet.
|
|
2500
|
-
*/
|
|
2501
382
|
async setupRouterOutlet(routerOutlet) {
|
|
2502
383
|
const canStart = () => {
|
|
2503
|
-
|
|
384
|
+
const config = getConfig();
|
|
385
|
+
const swipeEnabled = config && config.get('swipeBackEnabled', routerOutlet.mode === 'ios');
|
|
386
|
+
if (!swipeEnabled) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
2504
389
|
const { routeInfo } = this.props;
|
|
2505
|
-
const
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
const
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
enteringViewItem.
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
ionPageInDocument,
|
|
2527
|
-
canStartSwipe,
|
|
2528
|
-
}));
|
|
2529
|
-
return canStartSwipe;
|
|
390
|
+
const propsToUse = this.prevProps && this.prevProps.routeInfo.pathname === routeInfo.pushedByRoute
|
|
391
|
+
? this.prevProps.routeInfo
|
|
392
|
+
: { pathname: routeInfo.pushedByRoute || '' };
|
|
393
|
+
const enteringViewItem = this.context.findViewItemByRouteInfo(propsToUse, this.id, false);
|
|
394
|
+
return (!!enteringViewItem &&
|
|
395
|
+
/**
|
|
396
|
+
* The root url '/' is treated as
|
|
397
|
+
* the first view item (but is never mounted),
|
|
398
|
+
* so we do not want to swipe back to the
|
|
399
|
+
* root url.
|
|
400
|
+
*/
|
|
401
|
+
enteringViewItem.mount &&
|
|
402
|
+
/**
|
|
403
|
+
* When on the first page (whatever view
|
|
404
|
+
* you land on after the root url) it
|
|
405
|
+
* is possible for findViewItemByRouteInfo to
|
|
406
|
+
* return the exact same view you are currently on.
|
|
407
|
+
* Make sure that we are not swiping back to the same
|
|
408
|
+
* instances of a view.
|
|
409
|
+
*/
|
|
410
|
+
enteringViewItem.routeData.match.path !== routeInfo.pathname);
|
|
2530
411
|
};
|
|
2531
412
|
const onStart = async () => {
|
|
2532
|
-
var _a, _b, _c, _d, _e, _f;
|
|
2533
413
|
const { routeInfo } = this.props;
|
|
2534
|
-
const
|
|
2535
|
-
|
|
414
|
+
const propsToUse = this.prevProps && this.prevProps.routeInfo.pathname === routeInfo.pushedByRoute
|
|
415
|
+
? this.prevProps.routeInfo
|
|
416
|
+
: { pathname: routeInfo.pushedByRoute || '' };
|
|
417
|
+
const enteringViewItem = this.context.findViewItemByRouteInfo(propsToUse, this.id, false);
|
|
2536
418
|
const leavingViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id, false);
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
enteringViewId: enteringViewItem === null || enteringViewItem === void 0 ? void 0 : enteringViewItem.id,
|
|
2543
|
-
enteringViewPath: (_b = (_a = enteringViewItem === null || enteringViewItem === void 0 ? void 0 : enteringViewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path,
|
|
2544
|
-
enteringMount: enteringViewItem === null || enteringViewItem === void 0 ? void 0 : enteringViewItem.mount,
|
|
2545
|
-
hasEnteringIonPageElement: !!(enteringViewItem === null || enteringViewItem === void 0 ? void 0 : enteringViewItem.ionPageElement),
|
|
2546
|
-
leavingViewId: leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.id,
|
|
2547
|
-
}));
|
|
2548
|
-
// Ensure the entering view is mounted so React keeps rendering it during the gesture.
|
|
2549
|
-
// This is important when the view was previously marked for unmount but its
|
|
2550
|
-
// ionPageElement is still in the DOM.
|
|
2551
|
-
if (enteringViewItem && !enteringViewItem.mount) {
|
|
2552
|
-
enteringViewItem.mount = true;
|
|
2553
|
-
}
|
|
2554
|
-
// Reveal synchronously. `transitionPage` defers this behind async commit,
|
|
2555
|
-
// but the gesture's first progress frame fires in the same tick as onStart,
|
|
2556
|
-
// so an async reveal leaves the entering page hidden until the next frame.
|
|
2557
|
-
revealIonPageForSwipeBack(enteringViewItem === null || enteringViewItem === void 0 ? void 0 : enteringViewItem.ionPageElement);
|
|
2558
|
-
// When the gesture starts, kick off a transition controlled via swipe gesture
|
|
419
|
+
/**
|
|
420
|
+
* When the gesture starts, kick off
|
|
421
|
+
* a transition that is controlled
|
|
422
|
+
* via a swipe gesture.
|
|
423
|
+
*/
|
|
2559
424
|
if (enteringViewItem && leavingViewItem) {
|
|
2560
425
|
await this.transitionPage(routeInfo, enteringViewItem, leavingViewItem, 'back', true);
|
|
2561
426
|
}
|
|
2562
|
-
// eslint-disable-next-line no-console
|
|
2563
|
-
console.log('[SwipeBackOnStart:exit]', JSON.stringify({
|
|
2564
|
-
outletId: this.id,
|
|
2565
|
-
enteringFinalComputedDisplay: (enteringViewItem === null || enteringViewItem === void 0 ? void 0 : enteringViewItem.ionPageElement)
|
|
2566
|
-
? getComputedStyle(enteringViewItem.ionPageElement).display
|
|
2567
|
-
: null,
|
|
2568
|
-
enteringFinalInlineDisplay: (_d = (_c = enteringViewItem === null || enteringViewItem === void 0 ? void 0 : enteringViewItem.ionPageElement) === null || _c === void 0 ? void 0 : _c.style.display) !== null && _d !== void 0 ? _d : null,
|
|
2569
|
-
enteringFinalHiddenClass: (_f = (_e = enteringViewItem === null || enteringViewItem === void 0 ? void 0 : enteringViewItem.ionPageElement) === null || _e === void 0 ? void 0 : _e.classList.contains('ion-page-hidden')) !== null && _f !== void 0 ? _f : null,
|
|
2570
|
-
}));
|
|
2571
427
|
return Promise.resolve();
|
|
2572
428
|
};
|
|
2573
429
|
const onEnd = (shouldContinue) => {
|
|
2574
430
|
if (shouldContinue) {
|
|
2575
|
-
// User finished the swipe gesture, so complete the back navigation
|
|
2576
431
|
this.skipTransition = true;
|
|
2577
432
|
this.context.goBack();
|
|
2578
433
|
}
|
|
2579
434
|
else {
|
|
2580
|
-
|
|
435
|
+
/**
|
|
436
|
+
* In the event that the swipe
|
|
437
|
+
* gesture was aborted, we should
|
|
438
|
+
* re-hide the page that was going to enter.
|
|
439
|
+
*/
|
|
2581
440
|
const { routeInfo } = this.props;
|
|
2582
|
-
const
|
|
2583
|
-
|
|
441
|
+
const propsToUse = this.prevProps && this.prevProps.routeInfo.pathname === routeInfo.pushedByRoute
|
|
442
|
+
? this.prevProps.routeInfo
|
|
443
|
+
: { pathname: routeInfo.pushedByRoute || '' };
|
|
444
|
+
const enteringViewItem = this.context.findViewItemByRouteInfo(propsToUse, this.id, false);
|
|
2584
445
|
const leavingViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id, false);
|
|
2585
|
-
|
|
446
|
+
/**
|
|
447
|
+
* Ionic React has a design defect where it
|
|
448
|
+
* a) Unmounts the leaving view item when using parameterized routes
|
|
449
|
+
* b) Considers the current view to be the entering view when using
|
|
450
|
+
* parameterized routes
|
|
451
|
+
*
|
|
452
|
+
* As a result, we should not hide the view item here
|
|
453
|
+
* as it will cause the current view to be hidden.
|
|
454
|
+
*/
|
|
2586
455
|
if (enteringViewItem !== leavingViewItem && (enteringViewItem === null || enteringViewItem === void 0 ? void 0 : enteringViewItem.ionPageElement) !== undefined) {
|
|
2587
|
-
|
|
456
|
+
const { ionPageElement } = enteringViewItem;
|
|
457
|
+
ionPageElement.setAttribute('aria-hidden', 'true');
|
|
458
|
+
ionPageElement.classList.add('ion-page-hidden');
|
|
2588
459
|
}
|
|
2589
460
|
}
|
|
2590
461
|
};
|
|
@@ -2594,24 +465,7 @@ class StackManager extends React.PureComponent {
|
|
|
2594
465
|
onEnd,
|
|
2595
466
|
};
|
|
2596
467
|
}
|
|
2597
|
-
|
|
2598
|
-
* Animates the transition between the entering and leaving pages within the
|
|
2599
|
-
* router outlet.
|
|
2600
|
-
*
|
|
2601
|
-
* @param routeInfo Info about the current route.
|
|
2602
|
-
* @param enteringViewItem The view item that is entering.
|
|
2603
|
-
* @param leavingViewItem The view item that is leaving.
|
|
2604
|
-
* @param direction The direction of the transition.
|
|
2605
|
-
* @param progressAnimation Indicates if the transition is part of a
|
|
2606
|
-
* gesture controlled animation (e.g., swipe to go back).
|
|
2607
|
-
* Defaults to `false`.
|
|
2608
|
-
* @param skipAnimation When true, forces `duration: 0` so the page
|
|
2609
|
-
* swap is instant (no visible animation). Used for ionPage outlets
|
|
2610
|
-
* and back navigations that unmount the leaving view to prevent
|
|
2611
|
-
* overlapping content during the transition. Defaults to `false`.
|
|
2612
|
-
*/
|
|
2613
|
-
async transitionPage(routeInfo, enteringViewItem, leavingViewItem, direction, progressAnimation = false, skipAnimation = false) {
|
|
2614
|
-
const myGeneration = ++this.transitionGeneration;
|
|
468
|
+
async transitionPage(routeInfo, enteringViewItem, leavingViewItem, direction, progressAnimation = false) {
|
|
2615
469
|
const runCommit = async (enteringEl, leavingEl) => {
|
|
2616
470
|
const skipTransition = this.skipTransition;
|
|
2617
471
|
/**
|
|
@@ -2639,56 +493,24 @@ class StackManager extends React.PureComponent {
|
|
|
2639
493
|
}
|
|
2640
494
|
else {
|
|
2641
495
|
enteringEl.classList.add('ion-page');
|
|
2642
|
-
|
|
2643
|
-
* Only add ion-page-invisible if the element is not already visible.
|
|
2644
|
-
* During tab switches, the container page (e.g., TabContext wrapper) is
|
|
2645
|
-
* already visible and should remain so. Adding ion-page-invisible would
|
|
2646
|
-
* cause a flash where the visible page briefly becomes invisible.
|
|
2647
|
-
*/
|
|
2648
|
-
if (!isViewVisible(enteringEl)) {
|
|
2649
|
-
enteringEl.classList.add('ion-page-invisible');
|
|
2650
|
-
}
|
|
496
|
+
enteringEl.classList.add('ion-page-invisible');
|
|
2651
497
|
}
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
const commitPromise = routerOutlet.commit(enteringEl, leavingEl, {
|
|
2655
|
-
duration: commitDuration,
|
|
498
|
+
await routerOutlet.commit(enteringEl, leavingEl, {
|
|
499
|
+
duration: skipTransition || directionToUse === undefined ? 0 : undefined,
|
|
2656
500
|
direction: directionToUse,
|
|
2657
501
|
showGoBack: !!routeInfo.pushedByRoute,
|
|
2658
502
|
progressAnimation,
|
|
2659
503
|
animationBuilder: routeInfo.routeAnimation,
|
|
2660
504
|
});
|
|
2661
|
-
const timeoutMs = 5000;
|
|
2662
|
-
const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve('timeout'), timeoutMs));
|
|
2663
|
-
const result = await Promise.race([commitPromise.then(() => 'done'), timeoutPromise]);
|
|
2664
|
-
// Bail out if the component unmounted during the commit animation
|
|
2665
|
-
if (!this._isMounted)
|
|
2666
|
-
return;
|
|
2667
|
-
if (result === 'timeout') {
|
|
2668
|
-
// Force entering page visible even though commit hung
|
|
2669
|
-
enteringEl.classList.remove('ion-page-invisible');
|
|
2670
|
-
}
|
|
2671
|
-
/**
|
|
2672
|
-
* If a newer transitionPage call ran while this commit was in-flight (e.g., a tab
|
|
2673
|
-
* switch fired during a forward animation), the core commit may have applied
|
|
2674
|
-
* ion-page-hidden to leavingEl even though the newer transition already made it
|
|
2675
|
-
* visible. Undo that stale hide so the newer transition's DOM state wins.
|
|
2676
|
-
*/
|
|
2677
|
-
if (myGeneration !== this.transitionGeneration && leavingEl && leavingEl === this.transitionEnteringElement) {
|
|
2678
|
-
showIonPageElement(leavingEl);
|
|
2679
|
-
}
|
|
2680
|
-
if (!progressAnimation) {
|
|
2681
|
-
enteringEl.classList.remove('ion-page-invisible');
|
|
2682
|
-
}
|
|
2683
505
|
};
|
|
2684
506
|
const routerOutlet = this.routerOutletElement;
|
|
2685
507
|
const routeInfoFallbackDirection = routeInfo.routeDirection === 'none' || routeInfo.routeDirection === 'root' ? undefined : routeInfo.routeDirection;
|
|
2686
508
|
const directionToUse = direction !== null && direction !== void 0 ? direction : routeInfoFallbackDirection;
|
|
2687
509
|
if (enteringViewItem && enteringViewItem.ionPageElement && this.routerOutletElement) {
|
|
2688
|
-
this.transitionEnteringElement = enteringViewItem.ionPageElement;
|
|
2689
510
|
if (leavingViewItem && leavingViewItem.ionPageElement && enteringViewItem === leavingViewItem) {
|
|
2690
|
-
//
|
|
2691
|
-
|
|
511
|
+
// If a page is transitioning to another version of itself
|
|
512
|
+
// we clone it so we can have an animation to show
|
|
513
|
+
const match = matchComponent(leavingViewItem.reactElement, routeInfo.pathname, true);
|
|
2692
514
|
if (match) {
|
|
2693
515
|
const newLeavingElement = clonePageElement(leavingViewItem.ionPageElement.outerHTML);
|
|
2694
516
|
if (newLeavingElement) {
|
|
@@ -2698,111 +520,14 @@ class StackManager extends React.PureComponent {
|
|
|
2698
520
|
}
|
|
2699
521
|
}
|
|
2700
522
|
else {
|
|
2701
|
-
// Route no longer matches (e.g., /user/1 → /settings)
|
|
2702
523
|
await runCommit(enteringViewItem.ionPageElement, undefined);
|
|
2703
524
|
}
|
|
2704
525
|
}
|
|
2705
526
|
else {
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
if (isNonAnimatedTransition && leavingEl) {
|
|
2711
|
-
/**
|
|
2712
|
-
* Skip commit() for non-animated transitions (like tab switches).
|
|
2713
|
-
* commit() runs animation logic that can cause intermediate paints
|
|
2714
|
-
* even with duration: 0. Instead, swap visibility synchronously.
|
|
2715
|
-
*
|
|
2716
|
-
* Synchronous DOM class changes are batched into a single browser
|
|
2717
|
-
* paint, so there's no gap frame where neither page is visible and
|
|
2718
|
-
* no overlap frame where both pages are visible.
|
|
2719
|
-
*/
|
|
2720
|
-
const enteringEl = enteringViewItem.ionPageElement;
|
|
2721
|
-
// Ensure entering element has proper base classes
|
|
2722
|
-
enteringEl.classList.add('ion-page');
|
|
2723
|
-
// Clear ALL hidden state from entering element. showIonPageElement
|
|
2724
|
-
// removes visibility:hidden (from applySkipAnimationIfNeeded),
|
|
2725
|
-
// ion-page-hidden, and aria-hidden in one call.
|
|
2726
|
-
showIonPageElement(enteringEl);
|
|
2727
|
-
// Handle can-go-back class since we're skipping commit() which normally sets this
|
|
2728
|
-
if (routeInfo.pushedByRoute) {
|
|
2729
|
-
enteringEl.classList.add('can-go-back');
|
|
2730
|
-
}
|
|
2731
|
-
else {
|
|
2732
|
-
enteringEl.classList.remove('can-go-back');
|
|
2733
|
-
}
|
|
2734
|
-
/**
|
|
2735
|
-
* Wait for components to be ready. Menu buttons start hidden (menu-button-hidden)
|
|
2736
|
-
* and become visible after componentDidLoad. Wait for hydration and visibility.
|
|
2737
|
-
*/
|
|
2738
|
-
const waitForComponentsReady = () => {
|
|
2739
|
-
return new Promise((resolve) => {
|
|
2740
|
-
const checkReady = () => {
|
|
2741
|
-
const ionicComponents = enteringEl.querySelectorAll('ion-header, ion-toolbar, ion-buttons, ion-menu-button, ion-title, ion-content');
|
|
2742
|
-
const allHydrated = Array.from(ionicComponents).every((el) => el.classList.contains('hydrated'));
|
|
2743
|
-
const menuButtons = enteringEl.querySelectorAll('ion-menu-button');
|
|
2744
|
-
const menuButtonsReady = Array.from(menuButtons).every((el) => !el.classList.contains('menu-button-hidden'));
|
|
2745
|
-
return allHydrated && menuButtonsReady;
|
|
2746
|
-
};
|
|
2747
|
-
if (checkReady()) {
|
|
2748
|
-
resolve();
|
|
2749
|
-
return;
|
|
2750
|
-
}
|
|
2751
|
-
let resolved = false;
|
|
2752
|
-
const observer = new MutationObserver(() => {
|
|
2753
|
-
if (!resolved && checkReady()) {
|
|
2754
|
-
resolved = true;
|
|
2755
|
-
observer.disconnect();
|
|
2756
|
-
if (this.transitionObserver === observer) {
|
|
2757
|
-
this.transitionObserver = undefined;
|
|
2758
|
-
}
|
|
2759
|
-
resolve();
|
|
2760
|
-
}
|
|
2761
|
-
});
|
|
2762
|
-
// Disconnect any previous observer before tracking the new one
|
|
2763
|
-
if (this.transitionObserver) {
|
|
2764
|
-
this.transitionObserver.disconnect();
|
|
2765
|
-
}
|
|
2766
|
-
this.transitionObserver = observer;
|
|
2767
|
-
observer.observe(enteringEl, {
|
|
2768
|
-
subtree: true,
|
|
2769
|
-
attributes: true,
|
|
2770
|
-
attributeFilter: ['class'],
|
|
2771
|
-
});
|
|
2772
|
-
setTimeout(() => {
|
|
2773
|
-
if (!resolved) {
|
|
2774
|
-
resolved = true;
|
|
2775
|
-
observer.disconnect();
|
|
2776
|
-
if (this.transitionObserver === observer) {
|
|
2777
|
-
this.transitionObserver = undefined;
|
|
2778
|
-
}
|
|
2779
|
-
resolve();
|
|
2780
|
-
}
|
|
2781
|
-
}, 100);
|
|
2782
|
-
});
|
|
2783
|
-
};
|
|
2784
|
-
await waitForComponentsReady();
|
|
2785
|
-
// Bail out if the component unmounted during waitForComponentsReady
|
|
2786
|
-
if (!this._isMounted)
|
|
2787
|
-
return;
|
|
2788
|
-
// Swap visibility synchronously - show entering, hide leaving
|
|
2789
|
-
// Skip hiding if a newer transition already made leavingEl the entering view
|
|
2790
|
-
enteringEl.classList.remove('ion-page-invisible');
|
|
2791
|
-
if (myGeneration === this.transitionGeneration || leavingEl !== this.transitionEnteringElement) {
|
|
2792
|
-
leavingEl.classList.add('ion-page-hidden');
|
|
2793
|
-
leavingEl.setAttribute('aria-hidden', 'true');
|
|
2794
|
-
}
|
|
2795
|
-
}
|
|
2796
|
-
else {
|
|
2797
|
-
await runCommit(enteringViewItem.ionPageElement, leavingEl);
|
|
2798
|
-
if (leavingEl && !progressAnimation) {
|
|
2799
|
-
// Skip hiding if a newer transition already made leavingEl the entering view
|
|
2800
|
-
// runCommit's generation check has already restored its visibility in that case
|
|
2801
|
-
if (myGeneration === this.transitionGeneration || leavingEl !== this.transitionEnteringElement) {
|
|
2802
|
-
leavingEl.classList.add('ion-page-hidden');
|
|
2803
|
-
leavingEl.setAttribute('aria-hidden', 'true');
|
|
2804
|
-
}
|
|
2805
|
-
}
|
|
527
|
+
await runCommit(enteringViewItem.ionPageElement, leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.ionPageElement);
|
|
528
|
+
if (leavingViewItem && leavingViewItem.ionPageElement && !progressAnimation) {
|
|
529
|
+
leavingViewItem.ionPageElement.classList.add('ion-page-hidden');
|
|
530
|
+
leavingViewItem.ionPageElement.setAttribute('aria-hidden', 'true');
|
|
2806
531
|
}
|
|
2807
532
|
}
|
|
2808
533
|
}
|
|
@@ -2810,474 +535,193 @@ class StackManager extends React.PureComponent {
|
|
|
2810
535
|
render() {
|
|
2811
536
|
const { children } = this.props;
|
|
2812
537
|
const ionRouterOutlet = React.Children.only(children);
|
|
2813
|
-
// Store reference for use in getParentPath() and handlePageTransition()
|
|
2814
538
|
this.ionRouterOutlet = ionRouterOutlet;
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
return (React.createElement(StackContext.Provider, { value: this.stackContextValue }, React.cloneElement(ionRouterOutlet, {
|
|
2834
|
-
ref: (node) => {
|
|
2835
|
-
if (ionRouterOutlet.props.setRef) {
|
|
2836
|
-
// Needed to handle external refs from devs.
|
|
2837
|
-
ionRouterOutlet.props.setRef(node);
|
|
2838
|
-
}
|
|
2839
|
-
if (ionRouterOutlet.props.forwardedRef) {
|
|
2840
|
-
// Needed to handle external refs from devs.
|
|
2841
|
-
ionRouterOutlet.props.forwardedRef.current = node;
|
|
2842
|
-
}
|
|
2843
|
-
this.routerOutletElement = node;
|
|
2844
|
-
const { ref } = ionRouterOutlet;
|
|
2845
|
-
// Check for legacy refs.
|
|
2846
|
-
if (typeof ref === 'function') {
|
|
2847
|
-
ref(node);
|
|
2848
|
-
}
|
|
2849
|
-
},
|
|
2850
|
-
}, components)));
|
|
2851
|
-
}));
|
|
539
|
+
const components = this.context.getChildrenToRender(this.id, this.ionRouterOutlet, this.props.routeInfo, () => {
|
|
540
|
+
this.forceUpdate();
|
|
541
|
+
});
|
|
542
|
+
return (React.createElement(StackContext.Provider, { value: this.stackContextValue }, React.cloneElement(ionRouterOutlet, {
|
|
543
|
+
ref: (node) => {
|
|
544
|
+
if (ionRouterOutlet.props.setRef) {
|
|
545
|
+
ionRouterOutlet.props.setRef(node);
|
|
546
|
+
}
|
|
547
|
+
if (ionRouterOutlet.props.forwardedRef) {
|
|
548
|
+
ionRouterOutlet.props.forwardedRef.current = node;
|
|
549
|
+
}
|
|
550
|
+
this.routerOutletElement = node;
|
|
551
|
+
const { ref } = ionRouterOutlet;
|
|
552
|
+
if (typeof ref === 'function') {
|
|
553
|
+
ref(node);
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
}, components)));
|
|
2852
557
|
}
|
|
2853
558
|
static get contextType() {
|
|
2854
559
|
return RouteManagerContext;
|
|
2855
560
|
}
|
|
2856
561
|
}
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
* @param routeChildren The flat array of Route/IonRoute elements from the outlet.
|
|
2867
|
-
* @param basename The resolved parent path (without trailing slash or `/*`) used to relativize absolute paths.
|
|
2868
|
-
*/
|
|
2869
|
-
function routeElementsToRouteObjects(routeChildren, basename) {
|
|
2870
|
-
return routeChildren
|
|
2871
|
-
.filter((child) => child.props.path != null || child.props.index)
|
|
2872
|
-
.map((child) => {
|
|
2873
|
-
const handle = { _element: child };
|
|
2874
|
-
let path = child.props.path;
|
|
2875
|
-
// Relativize absolute paths by stripping the basename prefix
|
|
2876
|
-
if (path && path.startsWith('/') && basename) {
|
|
2877
|
-
if (path === basename) {
|
|
2878
|
-
path = '';
|
|
2879
|
-
}
|
|
2880
|
-
else if (path.startsWith(basename + '/')) {
|
|
2881
|
-
path = path.slice(basename.length + 1);
|
|
2882
|
-
}
|
|
2883
|
-
}
|
|
2884
|
-
if (child.props.index) {
|
|
2885
|
-
return {
|
|
2886
|
-
index: true,
|
|
2887
|
-
handle,
|
|
2888
|
-
caseSensitive: child.props.caseSensitive || undefined,
|
|
2889
|
-
};
|
|
562
|
+
function matchRoute(node, routeInfo) {
|
|
563
|
+
let matchedNode;
|
|
564
|
+
React.Children.forEach(node, (child) => {
|
|
565
|
+
const match = matchPath({
|
|
566
|
+
pathname: routeInfo.pathname,
|
|
567
|
+
componentProps: child.props,
|
|
568
|
+
});
|
|
569
|
+
if (match) {
|
|
570
|
+
matchedNode = child;
|
|
2890
571
|
}
|
|
2891
|
-
return {
|
|
2892
|
-
path,
|
|
2893
|
-
handle,
|
|
2894
|
-
caseSensitive: child.props.caseSensitive || undefined,
|
|
2895
|
-
};
|
|
2896
572
|
});
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
* Finds the `<Route />` node matching the current route info.
|
|
2900
|
-
* If no `<Route />` can be matched, a fallback node is returned.
|
|
2901
|
-
* Routes are prioritized by specificity (most specific first).
|
|
2902
|
-
*
|
|
2903
|
-
* @param node The root node to search for `<Route />` nodes.
|
|
2904
|
-
* @param routeInfo The route information to match against.
|
|
2905
|
-
* @param parentPath The parent path that was matched by the parent outlet (for nested routing)
|
|
2906
|
-
*/
|
|
2907
|
-
function findRouteByRouteInfo(node, routeInfo, parentPath) {
|
|
2908
|
-
var _a, _b, _c;
|
|
2909
|
-
let matchedNode;
|
|
2910
|
-
let fallbackNode;
|
|
2911
|
-
// `<Route />` nodes are rendered inside of a <Routes /> node
|
|
2912
|
-
const routesChildren = (_a = getRoutesChildren(node)) !== null && _a !== void 0 ? _a : node;
|
|
2913
|
-
// Collect all route children
|
|
2914
|
-
const routeChildren = React.Children.toArray(routesChildren).filter((child) => React.isValidElement(child) && (child.type === Route || child.type === IonRoute));
|
|
2915
|
-
// Delegate route matching to RR6's matchRoutes(), which handles specificity ranking internally.
|
|
2916
|
-
const basename = parentPath ? stripTrailingSlash(parentPath.replace('/*', '')) : undefined;
|
|
2917
|
-
const routeObjects = routeElementsToRouteObjects(routeChildren, basename);
|
|
2918
|
-
const matches = matchRoutes(routeObjects, { pathname: routeInfo.pathname }, basename);
|
|
2919
|
-
if (matches && matches.length > 0) {
|
|
2920
|
-
const bestMatch = matches[matches.length - 1];
|
|
2921
|
-
matchedNode = (_c = (_b = bestMatch.route.handle) === null || _b === void 0 ? void 0 : _b._element) !== null && _c !== void 0 ? _c : undefined;
|
|
573
|
+
if (matchedNode) {
|
|
574
|
+
return matchedNode;
|
|
2922
575
|
}
|
|
2923
|
-
//
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
if (
|
|
2927
|
-
|
|
2928
|
-
}
|
|
2929
|
-
else {
|
|
2930
|
-
const absolutePathRoutes = routeChildren.filter((r) => r.props.path && r.props.path.startsWith('/'));
|
|
2931
|
-
if (absolutePathRoutes.length > 0) {
|
|
2932
|
-
const absolutePaths = absolutePathRoutes.map((r) => r.props.path);
|
|
2933
|
-
const commonPrefix = computeCommonPrefix(absolutePaths);
|
|
2934
|
-
if (commonPrefix && commonPrefix !== '/') {
|
|
2935
|
-
pathnameInScope = routeInfo.pathname.startsWith(commonPrefix);
|
|
2936
|
-
}
|
|
2937
|
-
}
|
|
2938
|
-
}
|
|
2939
|
-
if (pathnameInScope) {
|
|
2940
|
-
for (const child of routeChildren) {
|
|
2941
|
-
if (!child.props.path) {
|
|
2942
|
-
fallbackNode = child;
|
|
2943
|
-
break;
|
|
2944
|
-
}
|
|
2945
|
-
}
|
|
576
|
+
// If we haven't found a node
|
|
577
|
+
// try to find one that doesn't have a path or from prop, that will be our not found route
|
|
578
|
+
React.Children.forEach(node, (child) => {
|
|
579
|
+
if (!(child.props.path || child.props.from)) {
|
|
580
|
+
matchedNode = child;
|
|
2946
581
|
}
|
|
2947
|
-
}
|
|
2948
|
-
return matchedNode
|
|
582
|
+
});
|
|
583
|
+
return matchedNode;
|
|
2949
584
|
}
|
|
2950
|
-
function matchComponent(node, pathname, forceExact
|
|
2951
|
-
var _a;
|
|
2952
|
-
const routePath = (_a = node === null || node === void 0 ? void 0 : node.props) === null || _a === void 0 ? void 0 : _a.path;
|
|
2953
|
-
let pathnameToMatch;
|
|
2954
|
-
if (parentPath && routePath && !routePath.startsWith('/')) {
|
|
2955
|
-
// When parent path is known, compute exact relative pathname
|
|
2956
|
-
const relative = pathname.startsWith(parentPath)
|
|
2957
|
-
? pathname.slice(parentPath.length).replace(/^\//, '')
|
|
2958
|
-
: pathname;
|
|
2959
|
-
pathnameToMatch = relative;
|
|
2960
|
-
}
|
|
2961
|
-
else {
|
|
2962
|
-
pathnameToMatch = derivePathnameToMatch(pathname, routePath);
|
|
2963
|
-
}
|
|
585
|
+
function matchComponent(node, pathname, forceExact) {
|
|
2964
586
|
return matchPath({
|
|
2965
|
-
pathname
|
|
2966
|
-
componentProps: Object.assign(Object.assign({}, node.props), {
|
|
587
|
+
pathname,
|
|
588
|
+
componentProps: Object.assign(Object.assign({}, node.props), { exact: forceExact }),
|
|
2967
589
|
});
|
|
2968
590
|
}
|
|
2969
591
|
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
walker = history.findLastLocation(walker);
|
|
3005
|
-
}
|
|
3006
|
-
return false;
|
|
3007
|
-
};
|
|
3008
|
-
const areParamsEqual = (a, b) => {
|
|
3009
|
-
const paramsA = a || {};
|
|
3010
|
-
const paramsB = b || {};
|
|
3011
|
-
const keysA = Object.keys(paramsA);
|
|
3012
|
-
const keysB = Object.keys(paramsB);
|
|
3013
|
-
if (keysA.length !== keysB.length) {
|
|
3014
|
-
return false;
|
|
592
|
+
class IonRouterInner extends React.PureComponent {
|
|
593
|
+
constructor(props) {
|
|
594
|
+
super(props);
|
|
595
|
+
this.exitViewFromOtherOutletHandlers = [];
|
|
596
|
+
this.locationHistory = new LocationHistory();
|
|
597
|
+
this.viewStack = new ReactRouterViewStack();
|
|
598
|
+
this.routeMangerContextState = {
|
|
599
|
+
canGoBack: () => this.locationHistory.canGoBack(),
|
|
600
|
+
clearOutlet: this.viewStack.clear,
|
|
601
|
+
findViewItemByPathname: this.viewStack.findViewItemByPathname,
|
|
602
|
+
getChildrenToRender: this.viewStack.getChildrenToRender,
|
|
603
|
+
goBack: () => this.handleNavigateBack(),
|
|
604
|
+
createViewItem: this.viewStack.createViewItem,
|
|
605
|
+
findViewItemByRouteInfo: this.viewStack.findViewItemByRouteInfo,
|
|
606
|
+
findLeavingViewItemByRouteInfo: this.viewStack.findLeavingViewItemByRouteInfo,
|
|
607
|
+
addViewItem: this.viewStack.add,
|
|
608
|
+
unMountViewItem: this.viewStack.remove,
|
|
609
|
+
};
|
|
610
|
+
const routeInfo = {
|
|
611
|
+
id: generateId('routeInfo'),
|
|
612
|
+
pathname: this.props.location.pathname,
|
|
613
|
+
search: this.props.location.search,
|
|
614
|
+
};
|
|
615
|
+
this.locationHistory.add(routeInfo);
|
|
616
|
+
this.handleChangeTab = this.handleChangeTab.bind(this);
|
|
617
|
+
this.handleResetTab = this.handleResetTab.bind(this);
|
|
618
|
+
this.handleNativeBack = this.handleNativeBack.bind(this);
|
|
619
|
+
this.handleNavigate = this.handleNavigate.bind(this);
|
|
620
|
+
this.handleNavigateBack = this.handleNavigateBack.bind(this);
|
|
621
|
+
this.props.registerHistoryListener(this.handleHistoryChange.bind(this));
|
|
622
|
+
this.handleSetCurrentTab = this.handleSetCurrentTab.bind(this);
|
|
623
|
+
this.state = {
|
|
624
|
+
routeInfo,
|
|
625
|
+
};
|
|
3015
626
|
}
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
const valueB = paramsB[key];
|
|
3019
|
-
if (Array.isArray(valueA) && Array.isArray(valueB)) {
|
|
3020
|
-
if (valueA.length !== valueB.length) {
|
|
3021
|
-
return false;
|
|
3022
|
-
}
|
|
3023
|
-
return valueA.every((entry, idx) => entry === valueB[idx]);
|
|
3024
|
-
}
|
|
3025
|
-
return valueA === valueB;
|
|
3026
|
-
});
|
|
3027
|
-
};
|
|
3028
|
-
const IonRouter = ({ children, registerHistoryListener }) => {
|
|
3029
|
-
const location = useLocation();
|
|
3030
|
-
const navigate = useNavigate();
|
|
3031
|
-
const didMountRef = useRef(false);
|
|
3032
|
-
const locationHistory = useRef(new LocationHistory());
|
|
3033
|
-
const currentTab = useRef(undefined);
|
|
3034
|
-
const viewStack = useRef(new ReactRouterViewStack());
|
|
3035
|
-
const incomingRouteParams = useRef(null);
|
|
3036
|
-
/**
|
|
3037
|
-
* Tracks location keys that the user navigated away from via browser back.
|
|
3038
|
-
* When a POP event's destination key matches the top of this stack, it's a
|
|
3039
|
-
* browser forward navigation. Uses React Router's unique location.key
|
|
3040
|
-
* instead of URLs to correctly handle duplicate URLs in history (e.g.,
|
|
3041
|
-
* navigating to /details, then /settings, then /details via routerLink,
|
|
3042
|
-
* then pressing back).
|
|
3043
|
-
* Cleared on PUSH (new navigation invalidates forward history).
|
|
3044
|
-
*/
|
|
3045
|
-
const forwardStack = useRef([]);
|
|
3046
|
-
/**
|
|
3047
|
-
* Tracks the current location key so we can push it onto the forward stack
|
|
3048
|
-
* when navigating back. Updated after each history change.
|
|
3049
|
-
*/
|
|
3050
|
-
const currentLocationKeyRef = useRef(location.key);
|
|
3051
|
-
const [routeInfo, setRouteInfo] = useState({
|
|
3052
|
-
id: generateId('routeInfo'),
|
|
3053
|
-
pathname: location.pathname,
|
|
3054
|
-
search: location.search,
|
|
3055
|
-
params: {},
|
|
3056
|
-
});
|
|
3057
|
-
useEffect(() => {
|
|
3058
|
-
if (didMountRef.current) {
|
|
627
|
+
handleChangeTab(tab, path, routeOptions) {
|
|
628
|
+
if (!path) {
|
|
3059
629
|
return;
|
|
3060
630
|
}
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
631
|
+
const routeInfo = this.locationHistory.getCurrentRouteInfoForTab(tab);
|
|
632
|
+
const [pathname, search] = path.split('?');
|
|
633
|
+
if (routeInfo) {
|
|
634
|
+
this.incomingRouteParams = Object.assign(Object.assign({}, routeInfo), { routeAction: 'push', routeDirection: 'none' });
|
|
635
|
+
if (routeInfo.pathname === pathname) {
|
|
636
|
+
this.incomingRouteParams.routeOptions = routeOptions;
|
|
637
|
+
this.props.history.push(routeInfo.pathname + (routeInfo.search || ''));
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
this.incomingRouteParams.pathname = pathname;
|
|
641
|
+
this.incomingRouteParams.search = search ? '?' + search : undefined;
|
|
642
|
+
this.incomingRouteParams.routeOptions = routeOptions;
|
|
643
|
+
this.props.history.push(pathname + (search ? '?' + search : ''));
|
|
3073
644
|
}
|
|
3074
645
|
}
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
646
|
+
else {
|
|
647
|
+
this.handleNavigate(pathname, 'push', 'none', undefined, routeOptions, tab);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
handleHistoryChange(location, action) {
|
|
651
|
+
var _a, _b, _c;
|
|
652
|
+
let leavingLocationInfo;
|
|
653
|
+
if (this.incomingRouteParams) {
|
|
654
|
+
if (this.incomingRouteParams.routeAction === 'replace') {
|
|
655
|
+
leavingLocationInfo = this.locationHistory.previous();
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
leavingLocationInfo = this.locationHistory.current();
|
|
3088
659
|
}
|
|
3089
|
-
const updatedRouteInfo = Object.assign(Object.assign({}, routeInfo), { params: paramsCopy });
|
|
3090
|
-
locationHistory.current.update(updatedRouteInfo);
|
|
3091
|
-
setRouteInfo(updatedRouteInfo);
|
|
3092
660
|
}
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
* or programmatic changes. It transforms the raw browser history changes
|
|
3097
|
-
* into `RouteInfo` objects, which are needed Ionic's animations and
|
|
3098
|
-
* navigation patterns.
|
|
3099
|
-
*
|
|
3100
|
-
* @param location The current location object from the history.
|
|
3101
|
-
* @param action The action that triggered the history change.
|
|
3102
|
-
*/
|
|
3103
|
-
const handleHistoryChange = (location, action) => {
|
|
3104
|
-
var _a, _b, _c, _d;
|
|
3105
|
-
/**
|
|
3106
|
-
* The leaving location is always the current route, for both programmatic
|
|
3107
|
-
* and external navigations. Using `previous()` for replace actions was
|
|
3108
|
-
* incorrect: it caused the equality check below to skip navigation when
|
|
3109
|
-
* the replace destination matched the entry two slots back in history.
|
|
3110
|
-
*/
|
|
3111
|
-
const leavingLocationInfo = locationHistory.current.current();
|
|
661
|
+
else {
|
|
662
|
+
leavingLocationInfo = this.locationHistory.current();
|
|
663
|
+
}
|
|
3112
664
|
const leavingUrl = leavingLocationInfo.pathname + leavingLocationInfo.search;
|
|
3113
|
-
if (leavingUrl !== location.pathname
|
|
3114
|
-
if (!incomingRouteParams
|
|
3115
|
-
// Use history-based tab detection instead of URL-pattern heuristics,
|
|
3116
|
-
// so tab routes work with any URL structure (not just paths containing "/tabs").
|
|
3117
|
-
// Fall back to currentTab.current only when the destination is within the
|
|
3118
|
-
// current tab's path hierarchy (prevents non-tab routes from inheriting a tab).
|
|
3119
|
-
let tabToUse = locationHistory.current.findTabForPathname(location.pathname);
|
|
3120
|
-
if (!tabToUse && currentTab.current) {
|
|
3121
|
-
const tabFirstRoute = locationHistory.current.getFirstRouteInfoForTab(currentTab.current);
|
|
3122
|
-
const tabRootPath = tabFirstRoute === null || tabFirstRoute === void 0 ? void 0 : tabFirstRoute.pathname;
|
|
3123
|
-
if (tabRootPath && (location.pathname === tabRootPath || location.pathname.startsWith(tabRootPath + '/'))) {
|
|
3124
|
-
tabToUse = currentTab.current;
|
|
3125
|
-
}
|
|
3126
|
-
}
|
|
3127
|
-
/**
|
|
3128
|
-
* A `REPLACE` action can be triggered by React Router's
|
|
3129
|
-
* `<Navigate />` component.
|
|
3130
|
-
*/
|
|
665
|
+
if (leavingUrl !== location.pathname) {
|
|
666
|
+
if (!this.incomingRouteParams) {
|
|
3131
667
|
if (action === 'REPLACE') {
|
|
3132
|
-
incomingRouteParams
|
|
668
|
+
this.incomingRouteParams = {
|
|
3133
669
|
routeAction: 'replace',
|
|
3134
670
|
routeDirection: 'none',
|
|
3135
|
-
tab:
|
|
671
|
+
tab: this.currentTab,
|
|
3136
672
|
};
|
|
3137
673
|
}
|
|
3138
|
-
/**
|
|
3139
|
-
* A `POP` action can be triggered by the browser's back/forward
|
|
3140
|
-
* button. Both fire as POP events, so we use a forward stack to
|
|
3141
|
-
* distinguish them: when going back, we push the leaving pathname
|
|
3142
|
-
* onto the stack. When the next POP's destination matches the top
|
|
3143
|
-
* of the stack, it's a forward navigation.
|
|
3144
|
-
*/
|
|
3145
674
|
if (action === 'POP') {
|
|
3146
|
-
const currentRoute = locationHistory.current
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
forwardStack.current.pop();
|
|
3151
|
-
incomingRouteParams.current = {
|
|
3152
|
-
routeAction: 'push',
|
|
3153
|
-
routeDirection: 'forward',
|
|
3154
|
-
tab: tabToUse,
|
|
3155
|
-
};
|
|
3156
|
-
}
|
|
3157
|
-
else if (currentRoute && currentRoute.pushedByRoute) {
|
|
3158
|
-
// Back navigation. Record current location key for potential forward
|
|
3159
|
-
forwardStack.current.push(currentLocationKeyRef.current);
|
|
3160
|
-
const prevInfo = locationHistory.current.findLastLocation(currentRoute);
|
|
3161
|
-
const isMultiStepBack = checkIsMultiStepBack(prevInfo, location.pathname, locationHistory.current);
|
|
3162
|
-
if (isMultiStepBack) {
|
|
3163
|
-
const destinationInfo = locationHistory.current.findLastLocationByPathname(location.pathname);
|
|
3164
|
-
incomingRouteParams.current = Object.assign(Object.assign({}, (destinationInfo || {})), { routeAction: 'pop', routeDirection: 'back' });
|
|
3165
|
-
}
|
|
3166
|
-
else if (prevInfo && prevInfo.pathname !== location.pathname && currentRoute.tab) {
|
|
3167
|
-
// Browser POP destination differs from within-tab back target.
|
|
3168
|
-
// Sync URL via replace, like handleNavigateBack's non-linear path (#25141).
|
|
3169
|
-
incomingRouteParams.current = Object.assign(Object.assign({}, prevInfo), { routeAction: 'pop', routeDirection: 'back' });
|
|
3170
|
-
forwardStack.current = [];
|
|
3171
|
-
handleNavigate(prevInfo.pathname + (prevInfo.search || ''), 'pop', 'back', undefined, undefined, prevInfo.tab);
|
|
3172
|
-
return;
|
|
3173
|
-
}
|
|
3174
|
-
else {
|
|
3175
|
-
incomingRouteParams.current = Object.assign(Object.assign({}, prevInfo), { routeAction: 'pop', routeDirection: 'back' });
|
|
3176
|
-
}
|
|
675
|
+
const currentRoute = this.locationHistory.current();
|
|
676
|
+
if (currentRoute && currentRoute.pushedByRoute) {
|
|
677
|
+
const prevInfo = this.locationHistory.findLastLocation(currentRoute);
|
|
678
|
+
this.incomingRouteParams = Object.assign(Object.assign({}, prevInfo), { routeAction: 'pop', routeDirection: 'back' });
|
|
3177
679
|
}
|
|
3178
680
|
else {
|
|
3179
|
-
|
|
3180
|
-
// Still push the current location key so browser forward is detectable.
|
|
3181
|
-
forwardStack.current.push(currentLocationKeyRef.current);
|
|
3182
|
-
incomingRouteParams.current = {
|
|
681
|
+
this.incomingRouteParams = {
|
|
3183
682
|
routeAction: 'pop',
|
|
3184
683
|
routeDirection: 'none',
|
|
3185
|
-
tab:
|
|
684
|
+
tab: this.currentTab,
|
|
3186
685
|
};
|
|
3187
686
|
}
|
|
3188
687
|
}
|
|
3189
|
-
if (!incomingRouteParams
|
|
3190
|
-
|
|
3191
|
-
incomingRouteParams.current = {
|
|
688
|
+
if (!this.incomingRouteParams) {
|
|
689
|
+
this.incomingRouteParams = {
|
|
3192
690
|
routeAction: 'push',
|
|
3193
|
-
routeDirection: (state === null ||
|
|
3194
|
-
routeOptions: state === null ||
|
|
3195
|
-
tab:
|
|
691
|
+
routeDirection: ((_a = location.state) === null || _a === void 0 ? void 0 : _a.direction) || 'forward',
|
|
692
|
+
routeOptions: (_b = location.state) === null || _b === void 0 ? void 0 : _b.routerOptions,
|
|
693
|
+
tab: this.currentTab,
|
|
3196
694
|
};
|
|
3197
695
|
}
|
|
3198
696
|
}
|
|
3199
|
-
// New navigation (PUSH) invalidates browser forward history,
|
|
3200
|
-
// so clear our forward stack to stay in sync.
|
|
3201
|
-
if (action === 'PUSH') {
|
|
3202
|
-
forwardStack.current = [];
|
|
3203
|
-
}
|
|
3204
697
|
let routeInfo;
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
}
|
|
3209
|
-
/**
|
|
3210
|
-
* An existing id indicates that it's re-activating an existing route.
|
|
3211
|
-
* e.g., tab switching or navigating back to a previous route
|
|
3212
|
-
*/
|
|
3213
|
-
if ((_a = incomingRouteParams.current) === null || _a === void 0 ? void 0 : _a.id) {
|
|
3214
|
-
routeInfo = Object.assign(Object.assign({}, incomingRouteParams.current), { lastPathname: leavingLocationInfo.pathname });
|
|
3215
|
-
locationHistory.current.add(routeInfo);
|
|
3216
|
-
/**
|
|
3217
|
-
* A new route is being created since it's not re-activating
|
|
3218
|
-
* an existing route.
|
|
3219
|
-
*/
|
|
698
|
+
if ((_c = this.incomingRouteParams) === null || _c === void 0 ? void 0 : _c.id) {
|
|
699
|
+
routeInfo = Object.assign(Object.assign({}, this.incomingRouteParams), { lastPathname: leavingLocationInfo.pathname });
|
|
700
|
+
this.locationHistory.add(routeInfo);
|
|
3220
701
|
}
|
|
3221
702
|
else {
|
|
3222
|
-
const isPushed =
|
|
3223
|
-
|
|
3224
|
-
routeInfo = Object.assign(Object.assign({ id: generateId('routeInfo') }, incomingRouteParams.current), { lastPathname: leavingLocationInfo.pathname, pathname: location.pathname, search: location.search, params: ((_c = incomingRouteParams.current) === null || _c === void 0 ? void 0 : _c.params)
|
|
3225
|
-
? filterUndefinedParams(incomingRouteParams.current.params)
|
|
3226
|
-
: {}, prevRouteLastPathname: leavingLocationInfo.lastPathname });
|
|
703
|
+
const isPushed = this.incomingRouteParams.routeAction === 'push' && this.incomingRouteParams.routeDirection === 'forward';
|
|
704
|
+
routeInfo = Object.assign(Object.assign({ id: generateId('routeInfo') }, this.incomingRouteParams), { lastPathname: leavingLocationInfo.pathname, pathname: location.pathname, search: location.search, params: this.props.match.params, prevRouteLastPathname: leavingLocationInfo.lastPathname });
|
|
3227
705
|
if (isPushed) {
|
|
3228
|
-
|
|
3229
|
-
// This preserves tab context for same-tab navigation while allowing cross-tab navigation.
|
|
3230
|
-
routeInfo.tab = routeInfo.tab || leavingLocationInfo.tab;
|
|
3231
|
-
routeInfo.pushedByRoute = leavingLocationInfo.pathname;
|
|
3232
|
-
}
|
|
3233
|
-
else if (routeInfo.routeAction === 'push' &&
|
|
3234
|
-
routeInfo.routeDirection === 'none' &&
|
|
3235
|
-
routeInfo.tab === leavingLocationInfo.tab) {
|
|
3236
|
-
// Push with routerDirection="none" within the same tab (or non-tab) context.
|
|
3237
|
-
// Still needs pushedByRoute so the back button can navigate back correctly.
|
|
3238
|
-
// Cross-tab navigations with direction "none" are handled by the tab-switching
|
|
3239
|
-
// block below which has different pushedByRoute semantics.
|
|
3240
|
-
routeInfo.tab = routeInfo.tab || leavingLocationInfo.tab;
|
|
706
|
+
routeInfo.tab = leavingLocationInfo.tab;
|
|
3241
707
|
routeInfo.pushedByRoute = leavingLocationInfo.pathname;
|
|
3242
708
|
}
|
|
3243
709
|
else if (routeInfo.routeAction === 'pop') {
|
|
3244
|
-
|
|
3245
|
-
// Find the route that pushed this one.
|
|
3246
|
-
const r = locationHistory.current.findLastLocation(routeInfo);
|
|
710
|
+
const r = this.locationHistory.findLastLocation(routeInfo);
|
|
3247
711
|
routeInfo.pushedByRoute = r === null || r === void 0 ? void 0 : r.pushedByRoute;
|
|
3248
|
-
// Navigating to a new tab.
|
|
3249
712
|
}
|
|
3250
713
|
else if (routeInfo.routeAction === 'push' && routeInfo.tab !== leavingLocationInfo.tab) {
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
*/
|
|
3255
|
-
const lastRoute = locationHistory.current.getCurrentRouteInfoForTab(routeInfo.tab);
|
|
3256
|
-
/**
|
|
3257
|
-
* Tab bar switches (direction 'none') should not create cross-tab back
|
|
3258
|
-
* navigation. Only inherit pushedByRoute from the tab's own history.
|
|
3259
|
-
*/
|
|
3260
|
-
if (routeInfo.routeDirection === 'none') {
|
|
3261
|
-
routeInfo.pushedByRoute = lastRoute === null || lastRoute === void 0 ? void 0 : lastRoute.pushedByRoute;
|
|
3262
|
-
}
|
|
3263
|
-
else {
|
|
3264
|
-
routeInfo.pushedByRoute = (_d = lastRoute === null || lastRoute === void 0 ? void 0 : lastRoute.pushedByRoute) !== null && _d !== void 0 ? _d : leavingLocationInfo.pathname;
|
|
3265
|
-
}
|
|
3266
|
-
// Triggered by `navigate()` with replace or a `<Navigate />` component, etc.
|
|
714
|
+
// If we are switching tabs grab the last route info for the tab and use its pushedByRoute
|
|
715
|
+
const lastRoute = this.locationHistory.getCurrentRouteInfoForTab(routeInfo.tab);
|
|
716
|
+
routeInfo.pushedByRoute = lastRoute === null || lastRoute === void 0 ? void 0 : lastRoute.pushedByRoute;
|
|
3267
717
|
}
|
|
3268
718
|
else if (routeInfo.routeAction === 'replace') {
|
|
719
|
+
// Make sure to set the lastPathname, etc.. to the current route so the page transitions out
|
|
720
|
+
const currentRouteInfo = this.locationHistory.current();
|
|
3269
721
|
/**
|
|
3270
|
-
*
|
|
3271
|
-
*
|
|
3272
|
-
|
|
3273
|
-
const currentRouteInfo = locationHistory.current.current();
|
|
3274
|
-
/**
|
|
3275
|
-
* Special handling for `replace` to ensure correct `pushedByRoute`
|
|
3276
|
-
* and `lastPathname`.
|
|
3277
|
-
*
|
|
3278
|
-
* If going from `/home` to `/child`, then replacing from
|
|
3279
|
-
* `/child` to `/home`, we don't want the route info to
|
|
3280
|
-
* say that `/home` was pushed by `/home` which is not correct.
|
|
722
|
+
* If going from /home to /child, then replacing from
|
|
723
|
+
* /child to /home, we don't want the route info to
|
|
724
|
+
* say that /home was pushed by /home which is not correct.
|
|
3281
725
|
*/
|
|
3282
726
|
const currentPushedBy = currentRouteInfo === null || currentRouteInfo === void 0 ? void 0 : currentRouteInfo.pushedByRoute;
|
|
3283
727
|
const pushedByRoute = currentPushedBy !== undefined && currentPushedBy !== routeInfo.pathname
|
|
@@ -3295,121 +739,46 @@ const IonRouter = ({ children, registerHistoryListener }) => {
|
|
|
3295
739
|
routeInfo.routeDirection = routeInfo.routeDirection || (currentRouteInfo === null || currentRouteInfo === void 0 ? void 0 : currentRouteInfo.routeDirection);
|
|
3296
740
|
routeInfo.routeAnimation = routeInfo.routeAnimation || (currentRouteInfo === null || currentRouteInfo === void 0 ? void 0 : currentRouteInfo.routeAnimation);
|
|
3297
741
|
}
|
|
3298
|
-
locationHistory.
|
|
742
|
+
this.locationHistory.add(routeInfo);
|
|
3299
743
|
}
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
// This ensures the forward stack records the correct key when navigating back.
|
|
3304
|
-
currentLocationKeyRef.current = location.key;
|
|
3305
|
-
incomingRouteParams.current = null;
|
|
3306
|
-
};
|
|
3307
|
-
/**
|
|
3308
|
-
* Resets the specified tab to its initial, root route.
|
|
3309
|
-
*
|
|
3310
|
-
* @param tab The tab to reset.
|
|
3311
|
-
* @param originalHref The original href for the tab.
|
|
3312
|
-
* @param originalRouteOptions The original route options for the tab.
|
|
3313
|
-
*/
|
|
3314
|
-
const handleResetTab = (tab, originalHref, originalRouteOptions) => {
|
|
3315
|
-
const routeInfo = locationHistory.current.getFirstRouteInfoForTab(tab);
|
|
3316
|
-
if (routeInfo) {
|
|
3317
|
-
const [pathname, search] = originalHref.split('?');
|
|
3318
|
-
const newRouteInfo = Object.assign({}, routeInfo);
|
|
3319
|
-
newRouteInfo.pathname = pathname;
|
|
3320
|
-
newRouteInfo.search = search ? '?' + search : '';
|
|
3321
|
-
newRouteInfo.routeOptions = originalRouteOptions;
|
|
3322
|
-
incomingRouteParams.current = Object.assign(Object.assign({}, newRouteInfo), { routeAction: 'pop', routeDirection: 'back' });
|
|
3323
|
-
navigate(newRouteInfo.pathname + (newRouteInfo.search || ''));
|
|
744
|
+
this.setState({
|
|
745
|
+
routeInfo,
|
|
746
|
+
});
|
|
3324
747
|
}
|
|
3325
|
-
|
|
748
|
+
this.incomingRouteParams = undefined;
|
|
749
|
+
}
|
|
3326
750
|
/**
|
|
3327
|
-
*
|
|
3328
|
-
*
|
|
3329
|
-
*
|
|
3330
|
-
*
|
|
3331
|
-
* @param routeOptions Additional route options.
|
|
751
|
+
* history@4.x uses goBack(), history@5.x uses back()
|
|
752
|
+
* TODO: If support for React Router <=5 is dropped
|
|
753
|
+
* this logic is no longer needed. We can just
|
|
754
|
+
* assume back() is available.
|
|
3332
755
|
*/
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
incomingRouteParams.current = Object.assign(Object.assign({}, routeParams), { search: newSearch || '', routeOptions });
|
|
3349
|
-
navigate(routeInfo.pathname + (newSearch || ''));
|
|
3350
|
-
/**
|
|
3351
|
-
* User is navigating to a different tab.
|
|
3352
|
-
* e.g., `/tabs/home` → `/tabs/settings`
|
|
3353
|
-
*/
|
|
3354
|
-
}
|
|
3355
|
-
else {
|
|
3356
|
-
incomingRouteParams.current = Object.assign(Object.assign({}, routeParams), { pathname, search: search ? '?' + search : '', routeOptions });
|
|
3357
|
-
navigate(pathname + (search ? '?' + search : ''));
|
|
3358
|
-
}
|
|
3359
|
-
// User has not navigated to this tab before.
|
|
756
|
+
handleNativeBack() {
|
|
757
|
+
const history = this.props.history;
|
|
758
|
+
const goBack = history.goBack || history.back;
|
|
759
|
+
goBack();
|
|
760
|
+
}
|
|
761
|
+
handleNavigate(path, routeAction, routeDirection, routeAnimation, routeOptions, tab) {
|
|
762
|
+
this.incomingRouteParams = Object.assign(this.incomingRouteParams || {}, {
|
|
763
|
+
routeAction,
|
|
764
|
+
routeDirection,
|
|
765
|
+
routeOptions,
|
|
766
|
+
routeAnimation,
|
|
767
|
+
tab,
|
|
768
|
+
});
|
|
769
|
+
if (routeAction === 'push') {
|
|
770
|
+
this.props.history.push(path);
|
|
3360
771
|
}
|
|
3361
772
|
else {
|
|
3362
|
-
|
|
3363
|
-
handleNavigate(fullPath, 'push', 'none', undefined, routeOptions, tab);
|
|
3364
|
-
}
|
|
3365
|
-
};
|
|
3366
|
-
/**
|
|
3367
|
-
* Set the current active tab in `locationHistory`.
|
|
3368
|
-
* This is crucial for maintaining tab history since each tab has
|
|
3369
|
-
* its own navigation stack.
|
|
3370
|
-
*
|
|
3371
|
-
* @param tab The tab to set as active.
|
|
3372
|
-
*/
|
|
3373
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
3374
|
-
const handleSetCurrentTab = (tab, _routeInfo) => {
|
|
3375
|
-
currentTab.current = tab;
|
|
3376
|
-
const current = locationHistory.current.current();
|
|
3377
|
-
if (!current) {
|
|
3378
|
-
// locationHistory not yet seeded (e.g., called during initial render
|
|
3379
|
-
// before mount effect). The mount effect will seed the correct entry.
|
|
3380
|
-
return;
|
|
3381
|
-
}
|
|
3382
|
-
const ri = Object.assign({}, current);
|
|
3383
|
-
if (ri.tab !== tab) {
|
|
3384
|
-
ri.tab = tab;
|
|
3385
|
-
locationHistory.current.update(ri);
|
|
773
|
+
this.props.history.replace(path);
|
|
3386
774
|
}
|
|
3387
|
-
}
|
|
3388
|
-
|
|
3389
|
-
* Handles the native back button press.
|
|
3390
|
-
* It's usually called when a user presses the platform-native back action.
|
|
3391
|
-
*/
|
|
3392
|
-
const handleNativeBack = () => {
|
|
3393
|
-
navigate(-1);
|
|
3394
|
-
};
|
|
3395
|
-
/**
|
|
3396
|
-
* Used to manage the back navigation within the Ionic React's routing
|
|
3397
|
-
* system. It's deeply integrated with Ionic's view lifecycle, animations,
|
|
3398
|
-
* and its custom history tracking (`locationHistory`) to provide a
|
|
3399
|
-
* native-like transition and maintain correct application state.
|
|
3400
|
-
*
|
|
3401
|
-
* @param defaultHref The fallback URL to navigate to if there's no
|
|
3402
|
-
* previous entry in the `locationHistory` stack.
|
|
3403
|
-
* @param routeAnimation A custom animation builder to override the
|
|
3404
|
-
* default "back" animation.
|
|
3405
|
-
*/
|
|
3406
|
-
const handleNavigateBack = (defaultHref, routeAnimation) => {
|
|
775
|
+
}
|
|
776
|
+
handleNavigateBack(defaultHref = '/', routeAnimation) {
|
|
3407
777
|
const config = getConfig();
|
|
3408
|
-
defaultHref = defaultHref
|
|
3409
|
-
const routeInfo = locationHistory.current
|
|
3410
|
-
// It's a linear navigation.
|
|
778
|
+
defaultHref = defaultHref ? defaultHref : config && config.get('backButtonDefaultHref');
|
|
779
|
+
const routeInfo = this.locationHistory.current();
|
|
3411
780
|
if (routeInfo && routeInfo.pushedByRoute) {
|
|
3412
|
-
const prevInfo = locationHistory.
|
|
781
|
+
const prevInfo = this.locationHistory.findLastLocation(routeInfo);
|
|
3413
782
|
if (prevInfo) {
|
|
3414
783
|
/**
|
|
3415
784
|
* This needs to be passed to handleNavigate
|
|
@@ -3417,280 +786,160 @@ const IonRouter = ({ children, registerHistoryListener }) => {
|
|
|
3417
786
|
* will be overridden.
|
|
3418
787
|
*/
|
|
3419
788
|
const incomingAnimation = routeAnimation || routeInfo.routeAnimation;
|
|
3420
|
-
incomingRouteParams
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
forwardStack.current.push(currentLocationKeyRef.current);
|
|
3430
|
-
navigate(-1);
|
|
3431
|
-
}
|
|
3432
|
-
else {
|
|
789
|
+
this.incomingRouteParams = Object.assign(Object.assign({}, prevInfo), { routeAction: 'pop', routeDirection: 'back', routeAnimation: incomingAnimation });
|
|
790
|
+
if (routeInfo.lastPathname === routeInfo.pushedByRoute ||
|
|
791
|
+
/**
|
|
792
|
+
* We need to exclude tab switches/tab
|
|
793
|
+
* context changes here because tabbed
|
|
794
|
+
* navigation is not linear, but router.back()
|
|
795
|
+
* will go back in a linear fashion.
|
|
796
|
+
*/
|
|
797
|
+
(prevInfo.pathname === routeInfo.pushedByRoute && routeInfo.tab === '' && prevInfo.tab === '')) {
|
|
3433
798
|
/**
|
|
3434
|
-
*
|
|
3435
|
-
*
|
|
3436
|
-
*
|
|
3437
|
-
*
|
|
799
|
+
* history@4.x uses goBack(), history@5.x uses back()
|
|
800
|
+
* TODO: If support for React Router <=5 is dropped
|
|
801
|
+
* this logic is no longer needed. We can just
|
|
802
|
+
* assume back() is available.
|
|
3438
803
|
*/
|
|
3439
|
-
|
|
3440
|
-
|
|
804
|
+
const history = this.props.history;
|
|
805
|
+
const goBack = history.goBack || history.back;
|
|
806
|
+
goBack();
|
|
807
|
+
}
|
|
808
|
+
else {
|
|
809
|
+
this.handleNavigate(prevInfo.pathname + (prevInfo.search || ''), 'pop', 'back', incomingAnimation);
|
|
3441
810
|
}
|
|
3442
|
-
/**
|
|
3443
|
-
* `pushedByRoute` exists, but no corresponding previous entry in
|
|
3444
|
-
* the history stack.
|
|
3445
|
-
*/
|
|
3446
811
|
}
|
|
3447
|
-
else
|
|
3448
|
-
handleNavigate(defaultHref, 'pop', 'back', routeAnimation);
|
|
812
|
+
else {
|
|
813
|
+
this.handleNavigate(defaultHref, 'pop', 'back', routeAnimation);
|
|
3449
814
|
}
|
|
3450
|
-
/**
|
|
3451
|
-
* No `pushedByRoute` (e.g., initial page load or tab root).
|
|
3452
|
-
* Navigate to defaultHref so the back button works on direct
|
|
3453
|
-
* deep-link loads (e.g., loading /tab1/child directly).
|
|
3454
|
-
* Only navigate when defaultHref is explicitly set. The core
|
|
3455
|
-
* back-button component hides itself when no defaultHref is
|
|
3456
|
-
* provided, so a click here means the user set one intentionally.
|
|
3457
|
-
*/
|
|
3458
815
|
}
|
|
3459
|
-
else
|
|
3460
|
-
handleNavigate(defaultHref, 'pop', 'back', routeAnimation);
|
|
816
|
+
else {
|
|
817
|
+
this.handleNavigate(defaultHref, 'pop', 'back', routeAnimation);
|
|
3461
818
|
}
|
|
3462
|
-
}
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
* @param routeOptions Additional options for the route.
|
|
3472
|
-
* @param tab The tab to navigate to, if applicable.
|
|
3473
|
-
*/
|
|
3474
|
-
const handleNavigate = (path, routeAction, routeDirection, routeAnimation, routeOptions, tab) => {
|
|
3475
|
-
var _a;
|
|
3476
|
-
const normalizedRouteDirection = routeAction === 'push' && routeDirection === undefined ? 'forward' : routeDirection;
|
|
3477
|
-
// When navigating from tabs context, we need to determine if the destination
|
|
3478
|
-
// is also within tabs. If not, we should clear the tab context.
|
|
3479
|
-
let navigationTab = tab;
|
|
3480
|
-
// If no explicit tab is provided and we're in a tab context,
|
|
3481
|
-
// check if the destination path is outside of the current tab context.
|
|
3482
|
-
// Uses history-based tab detection instead of URL pattern matching,
|
|
3483
|
-
// so it works with any tab URL structure.
|
|
3484
|
-
if (!tab && currentTab.current && path) {
|
|
3485
|
-
// Check if destination was previously visited in a tab context
|
|
3486
|
-
const destinationTab = locationHistory.current.findTabForPathname(path);
|
|
3487
|
-
if (destinationTab) {
|
|
3488
|
-
// Previously visited as a tab route - use the known tab
|
|
3489
|
-
navigationTab = destinationTab;
|
|
3490
|
-
}
|
|
3491
|
-
else {
|
|
3492
|
-
// New destination - check if it's a child of the current tab's root path
|
|
3493
|
-
const tabFirstRoute = locationHistory.current.getFirstRouteInfoForTab(currentTab.current);
|
|
3494
|
-
if (tabFirstRoute) {
|
|
3495
|
-
const tabRootPath = tabFirstRoute.pathname;
|
|
3496
|
-
if (path === tabRootPath || path.startsWith(tabRootPath + '/')) {
|
|
3497
|
-
// Still within the current tab's path hierarchy
|
|
3498
|
-
navigationTab = currentTab.current;
|
|
3499
|
-
}
|
|
3500
|
-
else {
|
|
3501
|
-
// Destination is outside the current tab context
|
|
3502
|
-
currentTab.current = undefined;
|
|
3503
|
-
navigationTab = undefined;
|
|
3504
|
-
}
|
|
3505
|
-
}
|
|
3506
|
-
}
|
|
819
|
+
}
|
|
820
|
+
handleResetTab(tab, originalHref, originalRouteOptions) {
|
|
821
|
+
const routeInfo = this.locationHistory.getFirstRouteInfoForTab(tab);
|
|
822
|
+
if (routeInfo) {
|
|
823
|
+
const newRouteInfo = Object.assign({}, routeInfo);
|
|
824
|
+
newRouteInfo.pathname = originalHref;
|
|
825
|
+
newRouteInfo.routeOptions = originalRouteOptions;
|
|
826
|
+
this.incomingRouteParams = Object.assign(Object.assign({}, newRouteInfo), { routeAction: 'pop', routeDirection: 'back' });
|
|
827
|
+
this.props.history.push(newRouteInfo.pathname + (newRouteInfo.search || ''));
|
|
3507
828
|
}
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
if (routeAction === 'replace') {
|
|
3516
|
-
const prevEntry = locationHistory.current.previous();
|
|
3517
|
-
const currentEntry = locationHistory.current.current();
|
|
3518
|
-
const prevPath = prevEntry ? prevEntry.pathname + (prevEntry.search || '') : undefined;
|
|
3519
|
-
if (prevEntry && currentEntry && prevEntry !== currentEntry && prevPath === path) {
|
|
3520
|
-
incomingRouteParams.current = Object.assign(Object.assign({}, prevEntry), { routeAction: 'replace', routeDirection: 'back', routeAnimation });
|
|
3521
|
-
forwardStack.current.push(currentLocationKeyRef.current);
|
|
3522
|
-
navigate(-1);
|
|
3523
|
-
return;
|
|
3524
|
-
}
|
|
829
|
+
}
|
|
830
|
+
handleSetCurrentTab(tab) {
|
|
831
|
+
this.currentTab = tab;
|
|
832
|
+
const ri = Object.assign({}, this.locationHistory.current());
|
|
833
|
+
if (ri.tab !== tab) {
|
|
834
|
+
ri.tab = tab;
|
|
835
|
+
this.locationHistory.update(ri);
|
|
3525
836
|
}
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
}
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
* canGoBack() returns false after the transition. All previously mounted views
|
|
3534
|
-
* are unmounted. Useful for post-login / post-logout root navigation.
|
|
3535
|
-
*
|
|
3536
|
-
* @param pathname The path to navigate to.
|
|
3537
|
-
* @param routeAnimation An optional custom animation builder.
|
|
3538
|
-
*/
|
|
3539
|
-
const handleNavigateRoot = (pathname, routeAnimation) => {
|
|
3540
|
-
currentTab.current = undefined;
|
|
3541
|
-
forwardStack.current = [];
|
|
3542
|
-
incomingRouteParams.current = {
|
|
3543
|
-
routeAction: 'replace',
|
|
3544
|
-
routeDirection: 'root',
|
|
3545
|
-
routeAnimation,
|
|
3546
|
-
};
|
|
3547
|
-
navigate(pathname, { replace: true });
|
|
3548
|
-
};
|
|
3549
|
-
const routeMangerContextValue = {
|
|
3550
|
-
canGoBack: () => locationHistory.current.canGoBack(),
|
|
3551
|
-
clearOutlet: viewStack.current.clear,
|
|
3552
|
-
findViewItemByPathname: viewStack.current.findViewItemByPathname,
|
|
3553
|
-
getChildrenToRender: viewStack.current.getChildrenToRender,
|
|
3554
|
-
getViewItemsForOutlet: viewStack.current.getViewItemsForOutlet.bind(viewStack.current),
|
|
3555
|
-
goBack: () => handleNavigateBack(),
|
|
3556
|
-
createViewItem: viewStack.current.createViewItem,
|
|
3557
|
-
findViewItemByRouteInfo: viewStack.current.findViewItemByRouteInfo,
|
|
3558
|
-
findLeavingViewItemByRouteInfo: viewStack.current.findLeavingViewItemByRouteInfo,
|
|
3559
|
-
addViewItem: viewStack.current.add,
|
|
3560
|
-
unMountViewItem: viewStack.current.remove,
|
|
3561
|
-
};
|
|
3562
|
-
return (React.createElement(RouteManagerContext.Provider, { value: routeMangerContextValue },
|
|
3563
|
-
React.createElement(NavManager, { ionRoute: IonRouteInner, stackManager: StackManager, routeInfo: routeInfo, onNativeBack: handleNativeBack, onNavigateBack: handleNavigateBack, onNavigate: handleNavigate, onNavigateRoot: handleNavigateRoot, onSetCurrentTab: handleSetCurrentTab, onChangeTab: handleChangeTab, onResetTab: handleResetTab, locationHistory: locationHistory.current }, children)));
|
|
3564
|
-
};
|
|
837
|
+
}
|
|
838
|
+
render() {
|
|
839
|
+
return (React.createElement(RouteManagerContext.Provider, { value: this.routeMangerContextState },
|
|
840
|
+
React.createElement(NavManager, { ionRoute: IonRouteInner, ionRedirect: {}, stackManager: StackManager, routeInfo: this.state.routeInfo, onNativeBack: this.handleNativeBack, onNavigateBack: this.handleNavigateBack, onNavigate: this.handleNavigate, onSetCurrentTab: this.handleSetCurrentTab, onChangeTab: this.handleChangeTab, onResetTab: this.handleResetTab, locationHistory: this.locationHistory }, this.props.children)));
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
const IonRouter = withRouter(IonRouterInner);
|
|
3565
844
|
IonRouter.displayName = 'IonRouter';
|
|
3566
845
|
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
* `useLocation` and `useNavigationType` are called within the valid
|
|
3576
|
-
* context of a `<BrowserRouter>`.
|
|
3577
|
-
*
|
|
3578
|
-
* It was split from `IonReactRouter` because these hooks must be
|
|
3579
|
-
* descendants of a `<Router>` component, which `BrowserRouter` provides.
|
|
3580
|
-
*/
|
|
3581
|
-
const RouterContent$2 = ({ children }) => {
|
|
3582
|
-
const location = useLocation();
|
|
3583
|
-
const navigationType = useNavigationType();
|
|
3584
|
-
const historyListenHandler = useRef();
|
|
3585
|
-
const registerHistoryListener = useCallback((cb) => {
|
|
3586
|
-
historyListenHandler.current = cb;
|
|
3587
|
-
}, []);
|
|
846
|
+
class IonReactRouter extends React.Component {
|
|
847
|
+
constructor(props) {
|
|
848
|
+
super(props);
|
|
849
|
+
const { history } = props, rest = __rest(props, ["history"]);
|
|
850
|
+
this.history = history || createBrowserHistory(rest);
|
|
851
|
+
this.history.listen(this.handleHistoryChange.bind(this));
|
|
852
|
+
this.registerHistoryListener = this.registerHistoryListener.bind(this);
|
|
853
|
+
}
|
|
3588
854
|
/**
|
|
3589
|
-
*
|
|
3590
|
-
*
|
|
3591
|
-
*
|
|
3592
|
-
*
|
|
3593
|
-
*
|
|
3594
|
-
*
|
|
3595
|
-
*
|
|
3596
|
-
* @param loc The current browser history location object.
|
|
3597
|
-
* @param act The type of navigation action ('PUSH', 'POP', or
|
|
3598
|
-
* 'REPLACE').
|
|
855
|
+
* history@4.x passes separate location and action
|
|
856
|
+
* params. history@5.x passes location and action
|
|
857
|
+
* together as a single object.
|
|
858
|
+
* TODO: If support for React Router <=5 is dropped
|
|
859
|
+
* this logic is no longer needed. We can just assume
|
|
860
|
+
* a single object with both location and action.
|
|
3599
861
|
*/
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
862
|
+
handleHistoryChange(location, action) {
|
|
863
|
+
const locationValue = location.location || location;
|
|
864
|
+
const actionValue = location.action || action;
|
|
865
|
+
if (this.historyListenHandler) {
|
|
866
|
+
this.historyListenHandler(locationValue, actionValue);
|
|
3603
867
|
}
|
|
3604
|
-
}
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
}
|
|
3608
|
-
|
|
3609
|
-
};
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
};
|
|
868
|
+
}
|
|
869
|
+
registerHistoryListener(cb) {
|
|
870
|
+
this.historyListenHandler = cb;
|
|
871
|
+
}
|
|
872
|
+
render() {
|
|
873
|
+
const _a = this.props, { children } = _a, props = __rest(_a, ["children"]);
|
|
874
|
+
return (React.createElement(Router, Object.assign({ history: this.history }, props),
|
|
875
|
+
React.createElement(IonRouter, { registerHistoryListener: this.registerHistoryListener }, children)));
|
|
876
|
+
}
|
|
877
|
+
}
|
|
3615
878
|
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
const navigationType = useNavigationType$1();
|
|
3624
|
-
const historyListenHandler = useRef();
|
|
3625
|
-
const registerHistoryListener = useCallback((cb) => {
|
|
3626
|
-
historyListenHandler.current = cb;
|
|
3627
|
-
}, []);
|
|
879
|
+
class IonReactMemoryRouter extends React.Component {
|
|
880
|
+
constructor(props) {
|
|
881
|
+
super(props);
|
|
882
|
+
this.history = props.history;
|
|
883
|
+
this.history.listen(this.handleHistoryChange.bind(this));
|
|
884
|
+
this.registerHistoryListener = this.registerHistoryListener.bind(this);
|
|
885
|
+
}
|
|
3628
886
|
/**
|
|
3629
|
-
*
|
|
3630
|
-
*
|
|
3631
|
-
*
|
|
3632
|
-
*
|
|
3633
|
-
*
|
|
3634
|
-
*
|
|
3635
|
-
*
|
|
3636
|
-
* @param location The current browser history location object.
|
|
3637
|
-
* @param action The type of navigation action ('PUSH', 'POP', or
|
|
3638
|
-
* 'REPLACE').
|
|
887
|
+
* history@4.x passes separate location and action
|
|
888
|
+
* params. history@5.x passes location and action
|
|
889
|
+
* together as a single object.
|
|
890
|
+
* TODO: If support for React Router <=5 is dropped
|
|
891
|
+
* this logic is no longer needed. We can just assume
|
|
892
|
+
* a single object with both location and action.
|
|
3639
893
|
*/
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
894
|
+
handleHistoryChange(location, action) {
|
|
895
|
+
const locationValue = location.location || location;
|
|
896
|
+
const actionValue = location.action || action;
|
|
897
|
+
if (this.historyListenHandler) {
|
|
898
|
+
this.historyListenHandler(locationValue, actionValue);
|
|
3643
899
|
}
|
|
3644
|
-
}
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
}
|
|
3648
|
-
|
|
3649
|
-
};
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
};
|
|
900
|
+
}
|
|
901
|
+
registerHistoryListener(cb) {
|
|
902
|
+
this.historyListenHandler = cb;
|
|
903
|
+
}
|
|
904
|
+
render() {
|
|
905
|
+
const _a = this.props, { children } = _a, props = __rest(_a, ["children"]);
|
|
906
|
+
return (React.createElement(Router$1, Object.assign({}, props),
|
|
907
|
+
React.createElement(IonRouter, { registerHistoryListener: this.registerHistoryListener }, children)));
|
|
908
|
+
}
|
|
909
|
+
}
|
|
3655
910
|
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
const registerHistoryListener = useCallback((cb) => {
|
|
3665
|
-
historyListenHandler.current = cb;
|
|
3666
|
-
}, []);
|
|
911
|
+
class IonReactHashRouter extends React.Component {
|
|
912
|
+
constructor(props) {
|
|
913
|
+
super(props);
|
|
914
|
+
const { history } = props, rest = __rest(props, ["history"]);
|
|
915
|
+
this.history = history || createHashHistory(rest);
|
|
916
|
+
this.history.listen(this.handleHistoryChange.bind(this));
|
|
917
|
+
this.registerHistoryListener = this.registerHistoryListener.bind(this);
|
|
918
|
+
}
|
|
3667
919
|
/**
|
|
3668
|
-
*
|
|
3669
|
-
*
|
|
3670
|
-
*
|
|
3671
|
-
*
|
|
3672
|
-
*
|
|
3673
|
-
*
|
|
3674
|
-
*
|
|
3675
|
-
* @param location The current browser history location object.
|
|
3676
|
-
* @param action The type of navigation action ('PUSH', 'POP', or
|
|
3677
|
-
* 'REPLACE').
|
|
920
|
+
* history@4.x passes separate location and action
|
|
921
|
+
* params. history@5.x passes location and action
|
|
922
|
+
* together as a single object.
|
|
923
|
+
* TODO: If support for React Router <=5 is dropped
|
|
924
|
+
* this logic is no longer needed. We can just assume
|
|
925
|
+
* a single object with both location and action.
|
|
3678
926
|
*/
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
927
|
+
handleHistoryChange(location, action) {
|
|
928
|
+
const locationValue = location.location || location;
|
|
929
|
+
const actionValue = location.action || action;
|
|
930
|
+
if (this.historyListenHandler) {
|
|
931
|
+
this.historyListenHandler(locationValue, actionValue);
|
|
3682
932
|
}
|
|
3683
|
-
}
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
}
|
|
3687
|
-
|
|
3688
|
-
};
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
};
|
|
933
|
+
}
|
|
934
|
+
registerHistoryListener(cb) {
|
|
935
|
+
this.historyListenHandler = cb;
|
|
936
|
+
}
|
|
937
|
+
render() {
|
|
938
|
+
const _a = this.props, { children } = _a, props = __rest(_a, ["children"]);
|
|
939
|
+
return (React.createElement(Router, Object.assign({ history: this.history }, props),
|
|
940
|
+
React.createElement(IonRouter, { registerHistoryListener: this.registerHistoryListener }, children)));
|
|
941
|
+
}
|
|
942
|
+
}
|
|
3694
943
|
|
|
3695
944
|
export { IonReactHashRouter, IonReactMemoryRouter, IonReactRouter };
|
|
3696
945
|
//# sourceMappingURL=index.js.map
|