@ionic/react-router 8.7.13-dev.11765829391.14bc580c → 8.7.13-dev.11765920447.1a01ab8b
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 +540 -2399
- 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 -59
- 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 -53
- 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 -10
package/dist/index.js
CHANGED
|
@@ -1,1128 +1,176 @@
|
|
|
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;
|
|
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,
|
|
113
13
|
}
|
|
114
|
-
|
|
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('/');
|
|
14
|
+
: {}))));
|
|
122
15
|
}
|
|
123
|
-
|
|
124
|
-
};
|
|
16
|
+
}
|
|
125
17
|
|
|
126
18
|
/**
|
|
127
|
-
*
|
|
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 route path is a "splat-only" route (just `*` or `/*`).
|
|
167
|
-
*/
|
|
168
|
-
const isSplatOnlyRoute = (routePath) => {
|
|
169
|
-
return routePath === '*' || routePath === '/*';
|
|
170
|
-
};
|
|
171
|
-
/**
|
|
172
|
-
* Checks if a route has an embedded wildcard (e.g., "tab1/*" but not "*" or "/*").
|
|
19
|
+
* @see https://v5.reactrouter.com/web/api/matchPath
|
|
173
20
|
*/
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
*/
|
|
190
|
-
const isSpecificRouteMatch = (route, remainingPath) => {
|
|
191
|
-
const routePath = route.props.path;
|
|
192
|
-
if (route.props.index || isSplatOnlyRoute(routePath)) {
|
|
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 }`.
|
|
28
|
+
*/
|
|
29
|
+
const matchProps = {
|
|
30
|
+
exact,
|
|
31
|
+
path,
|
|
32
|
+
component,
|
|
33
|
+
};
|
|
34
|
+
const match = matchPath$1(pathname, matchProps);
|
|
35
|
+
if (!match) {
|
|
193
36
|
return false;
|
|
194
37
|
}
|
|
195
|
-
return
|
|
196
|
-
};
|
|
197
|
-
/**
|
|
198
|
-
* Analyzes route children to determine their characteristics.
|
|
199
|
-
*
|
|
200
|
-
* @param routeChildren The route children to analyze.
|
|
201
|
-
* @returns Analysis of the route characteristics.
|
|
202
|
-
*/
|
|
203
|
-
const analyzeRouteChildren = (routeChildren) => {
|
|
204
|
-
const hasRelativeRoutes = routeChildren.some((route) => {
|
|
205
|
-
const path = route.props.path;
|
|
206
|
-
return path && !path.startsWith('/') && path !== '*';
|
|
207
|
-
});
|
|
208
|
-
const hasIndexRoute = routeChildren.some((route) => route.props.index);
|
|
209
|
-
const hasWildcardRoute = routeChildren.some((route) => {
|
|
210
|
-
const routePath = route.props.path;
|
|
211
|
-
return routePath === '*' || routePath === '/*';
|
|
212
|
-
});
|
|
213
|
-
return { hasRelativeRoutes, hasIndexRoute, hasWildcardRoute, routeChildren };
|
|
214
|
-
};
|
|
215
|
-
/**
|
|
216
|
-
* Computes the parent path for a nested outlet based on the current pathname
|
|
217
|
-
* and the outlet's route configuration.
|
|
218
|
-
*
|
|
219
|
-
* The algorithm finds the shortest parent path where a route matches the remaining path.
|
|
220
|
-
* Priority: specific routes > wildcard routes > index routes (only at mount point)
|
|
221
|
-
*
|
|
222
|
-
* @param options The options for computing the parent path.
|
|
223
|
-
* @returns The computed parent path result.
|
|
224
|
-
*/
|
|
225
|
-
const computeParentPath = (options) => {
|
|
226
|
-
const { currentPathname, outletMountPath, routeChildren, hasRelativeRoutes, hasIndexRoute, hasWildcardRoute } = options;
|
|
227
|
-
// If this outlet previously established a mount path and the current
|
|
228
|
-
// pathname is outside of that scope, do not attempt to re-compute a new
|
|
229
|
-
// parent path.
|
|
230
|
-
if (outletMountPath && !currentPathname.startsWith(outletMountPath)) {
|
|
231
|
-
return { parentPath: undefined, outletMountPath };
|
|
232
|
-
}
|
|
233
|
-
if ((hasRelativeRoutes || hasIndexRoute) && currentPathname.includes('/')) {
|
|
234
|
-
const segments = currentPathname.split('/').filter(Boolean);
|
|
235
|
-
if (segments.length >= 1) {
|
|
236
|
-
// Find matches at each level, keeping track of the FIRST (shortest) match
|
|
237
|
-
let firstSpecificMatch = undefined;
|
|
238
|
-
let firstWildcardMatch = undefined;
|
|
239
|
-
let indexMatchAtMount = undefined;
|
|
240
|
-
// Start at i = 1 (normal case: strip at least one segment for parent path)
|
|
241
|
-
for (let i = 1; i <= segments.length; i++) {
|
|
242
|
-
const parentPath = '/' + segments.slice(0, i).join('/');
|
|
243
|
-
const remainingPath = segments.slice(i).join('/');
|
|
244
|
-
// Check for specific route matches (non-wildcard-only, non-index)
|
|
245
|
-
// Also check routes with embedded wildcards (e.g., "tab1/*")
|
|
246
|
-
const hasSpecificMatch = routeChildren.some((route) => isSpecificRouteMatch(route, remainingPath) || matchesEmbeddedWildcardRoute(route, remainingPath));
|
|
247
|
-
if (hasSpecificMatch && !firstSpecificMatch) {
|
|
248
|
-
firstSpecificMatch = parentPath;
|
|
249
|
-
// Found a specific match - this is our answer for non-index routes
|
|
250
|
-
break;
|
|
251
|
-
}
|
|
252
|
-
// Check if wildcard would match this remaining path
|
|
253
|
-
// Only if remaining is non-empty (wildcard needs something to match)
|
|
254
|
-
if (remainingPath !== '' && remainingPath !== '/' && hasWildcardRoute && !firstWildcardMatch) {
|
|
255
|
-
// Check if any specific route could plausibly match this remaining path
|
|
256
|
-
const remainingFirstSegment = remainingPath.split('/')[0];
|
|
257
|
-
const couldAnyRouteMatch = routeChildren.some((route) => {
|
|
258
|
-
const routePath = route.props.path;
|
|
259
|
-
if (!routePath || routePath === '*' || routePath === '/*')
|
|
260
|
-
return false;
|
|
261
|
-
if (route.props.index)
|
|
262
|
-
return false;
|
|
263
|
-
const routeFirstSegment = routePath.split('/')[0].replace(/[*:]/g, '');
|
|
264
|
-
if (!routeFirstSegment)
|
|
265
|
-
return false;
|
|
266
|
-
// Check for prefix overlap (either direction)
|
|
267
|
-
return (routeFirstSegment.startsWith(remainingFirstSegment.slice(0, 3)) ||
|
|
268
|
-
remainingFirstSegment.startsWith(routeFirstSegment.slice(0, 3)));
|
|
269
|
-
});
|
|
270
|
-
// Only save wildcard match if no specific route could match
|
|
271
|
-
if (!couldAnyRouteMatch) {
|
|
272
|
-
firstWildcardMatch = parentPath;
|
|
273
|
-
// Continue looking - might find a specific match at a longer path
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
// Check for index route match when remaining path is empty
|
|
277
|
-
// BUT only at the outlet's mount path level
|
|
278
|
-
if ((remainingPath === '' || remainingPath === '/') && hasIndexRoute) {
|
|
279
|
-
// Index route matches when current path exactly matches the mount path
|
|
280
|
-
// If we already have an outletMountPath, index should only match there
|
|
281
|
-
if (outletMountPath) {
|
|
282
|
-
if (parentPath === outletMountPath) {
|
|
283
|
-
indexMatchAtMount = parentPath;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
else {
|
|
287
|
-
// No mount path set yet - index would establish this as mount path
|
|
288
|
-
// But only if we haven't found a better match
|
|
289
|
-
indexMatchAtMount = parentPath;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
// Fallback: check at root level (i = 0) for embedded wildcard routes.
|
|
294
|
-
// This handles outlets inside root-level splat routes where routes like
|
|
295
|
-
// "tab1/*" need to match the full pathname.
|
|
296
|
-
if (!firstSpecificMatch) {
|
|
297
|
-
const fullRemainingPath = segments.join('/');
|
|
298
|
-
const hasRootLevelMatch = routeChildren.some((route) => matchesEmbeddedWildcardRoute(route, fullRemainingPath));
|
|
299
|
-
if (hasRootLevelMatch) {
|
|
300
|
-
firstSpecificMatch = '/';
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
// Determine the best parent path:
|
|
304
|
-
// 1. Specific match (routes like tabs/*, favorites) - highest priority
|
|
305
|
-
// 2. Wildcard match (route path="*") - catches unmatched segments
|
|
306
|
-
// 3. Index match - only valid at the outlet's mount point, not deeper
|
|
307
|
-
let bestPath = undefined;
|
|
308
|
-
if (firstSpecificMatch) {
|
|
309
|
-
bestPath = firstSpecificMatch;
|
|
310
|
-
}
|
|
311
|
-
else if (firstWildcardMatch) {
|
|
312
|
-
bestPath = firstWildcardMatch;
|
|
313
|
-
}
|
|
314
|
-
else if (indexMatchAtMount) {
|
|
315
|
-
// Only use index match if no specific or wildcard matched
|
|
316
|
-
// This handles the case where pathname exactly matches the mount path
|
|
317
|
-
bestPath = indexMatchAtMount;
|
|
318
|
-
}
|
|
319
|
-
// Store the mount path when we first successfully match a route
|
|
320
|
-
let newOutletMountPath = outletMountPath;
|
|
321
|
-
if (!outletMountPath && bestPath) {
|
|
322
|
-
newOutletMountPath = bestPath;
|
|
323
|
-
}
|
|
324
|
-
// If we have a mount path, verify the current pathname is within scope
|
|
325
|
-
if (newOutletMountPath && !currentPathname.startsWith(newOutletMountPath)) {
|
|
326
|
-
return { parentPath: undefined, outletMountPath: newOutletMountPath };
|
|
327
|
-
}
|
|
328
|
-
return { parentPath: bestPath, outletMountPath: newOutletMountPath };
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
// Handle outlets with ONLY absolute routes (no relative routes or index routes)
|
|
332
|
-
// Compute the common prefix of all absolute routes to determine the outlet's scope
|
|
333
|
-
if (!hasRelativeRoutes && !hasIndexRoute) {
|
|
334
|
-
const absolutePathRoutes = routeChildren.filter((route) => {
|
|
335
|
-
const path = route.props.path;
|
|
336
|
-
return path && path.startsWith('/');
|
|
337
|
-
});
|
|
338
|
-
if (absolutePathRoutes.length > 0) {
|
|
339
|
-
const absolutePaths = absolutePathRoutes.map((r) => r.props.path);
|
|
340
|
-
const commonPrefix = computeCommonPrefix(absolutePaths);
|
|
341
|
-
if (commonPrefix && commonPrefix !== '/') {
|
|
342
|
-
// Set the mount path based on common prefix of absolute routes
|
|
343
|
-
const newOutletMountPath = outletMountPath || commonPrefix;
|
|
344
|
-
// Check if current pathname is within scope
|
|
345
|
-
if (!currentPathname.startsWith(commonPrefix)) {
|
|
346
|
-
return { parentPath: undefined, outletMountPath: newOutletMountPath };
|
|
347
|
-
}
|
|
348
|
-
return { parentPath: commonPrefix, outletMountPath: newOutletMountPath };
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
return { parentPath: outletMountPath, outletMountPath };
|
|
353
|
-
};
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Ensures the given path has a leading slash.
|
|
357
|
-
*
|
|
358
|
-
* @param value The path string to normalize.
|
|
359
|
-
* @returns The path with a leading slash.
|
|
360
|
-
*/
|
|
361
|
-
const ensureLeadingSlash = (value) => {
|
|
362
|
-
if (value === '') {
|
|
363
|
-
return '/';
|
|
364
|
-
}
|
|
365
|
-
return value.startsWith('/') ? value : `/${value}`;
|
|
366
|
-
};
|
|
367
|
-
/**
|
|
368
|
-
* Strips the trailing slash from a path, unless it's the root path.
|
|
369
|
-
*
|
|
370
|
-
* @param value The path string to normalize.
|
|
371
|
-
* @returns The path without a trailing slash.
|
|
372
|
-
*/
|
|
373
|
-
const stripTrailingSlash = (value) => {
|
|
374
|
-
return value.length > 1 && value.endsWith('/') ? value.slice(0, -1) : value;
|
|
375
|
-
};
|
|
376
|
-
/**
|
|
377
|
-
* Normalizes a pathname for comparison by ensuring a leading slash
|
|
378
|
-
* and removing trailing slashes.
|
|
379
|
-
*
|
|
380
|
-
* @param value The pathname to normalize, can be undefined.
|
|
381
|
-
* @returns A normalized pathname string.
|
|
382
|
-
*/
|
|
383
|
-
const normalizePathnameForComparison = (value) => {
|
|
384
|
-
if (!value || value === '') {
|
|
385
|
-
return '/';
|
|
386
|
-
}
|
|
387
|
-
const withLeadingSlash = ensureLeadingSlash(value);
|
|
388
|
-
return stripTrailingSlash(withLeadingSlash);
|
|
38
|
+
return match;
|
|
389
39
|
};
|
|
390
40
|
|
|
391
|
-
/**
|
|
392
|
-
* Extracts the children from a Routes wrapper component.
|
|
393
|
-
* The use of `<Routes />` is encouraged with React Router v6.
|
|
394
|
-
*
|
|
395
|
-
* @param node The React node to extract Routes children from.
|
|
396
|
-
* @returns The children of the Routes component, or undefined if not found.
|
|
397
|
-
*/
|
|
398
|
-
const getRoutesChildren = (node) => {
|
|
399
|
-
let routesNode;
|
|
400
|
-
React.Children.forEach(node, (child) => {
|
|
401
|
-
if (child.type === Routes) {
|
|
402
|
-
routesNode = child;
|
|
403
|
-
}
|
|
404
|
-
});
|
|
405
|
-
if (routesNode) {
|
|
406
|
-
// The children of the `<Routes />` component are most likely
|
|
407
|
-
// (and should be) the `<Route />` components.
|
|
408
|
-
return routesNode.props.children;
|
|
409
|
-
}
|
|
410
|
-
return undefined;
|
|
411
|
-
};
|
|
412
|
-
/**
|
|
413
|
-
* Extracts Route children from a node (either directly or from a Routes wrapper).
|
|
414
|
-
*
|
|
415
|
-
* @param children The children to extract routes from.
|
|
416
|
-
* @returns An array of Route elements.
|
|
417
|
-
*/
|
|
418
|
-
const extractRouteChildren = (children) => {
|
|
419
|
-
var _a;
|
|
420
|
-
const routesChildren = (_a = getRoutesChildren(children)) !== null && _a !== void 0 ? _a : children;
|
|
421
|
-
return React.Children.toArray(routesChildren).filter((child) => React.isValidElement(child) && child.type === Route);
|
|
422
|
-
};
|
|
423
|
-
/**
|
|
424
|
-
* Checks if a React element is a Navigate component (redirect).
|
|
425
|
-
*
|
|
426
|
-
* @param element The element to check.
|
|
427
|
-
* @returns True if the element is a Navigate component.
|
|
428
|
-
*/
|
|
429
|
-
const isNavigateElement = (element) => {
|
|
430
|
-
return (React.isValidElement(element) &&
|
|
431
|
-
(element.type === Navigate || (typeof element.type === 'function' && element.type.name === 'Navigate')));
|
|
432
|
-
};
|
|
433
|
-
|
|
434
|
-
/**
|
|
435
|
-
* Sorts view items by route specificity (most specific first).
|
|
436
|
-
* - Exact matches (no wildcards/params) come first
|
|
437
|
-
* - Among wildcard routes, longer paths are more specific
|
|
438
|
-
*
|
|
439
|
-
* @param views The view items to sort.
|
|
440
|
-
* @returns A new sorted array of view items.
|
|
441
|
-
*/
|
|
442
|
-
const sortViewsBySpecificity = (views) => {
|
|
443
|
-
return [...views].sort((a, b) => {
|
|
444
|
-
var _a, _b, _c, _d;
|
|
445
|
-
const pathA = ((_b = (_a = a.routeData) === null || _a === void 0 ? void 0 : _a.childProps) === null || _b === void 0 ? void 0 : _b.path) || '';
|
|
446
|
-
const pathB = ((_d = (_c = b.routeData) === null || _c === void 0 ? void 0 : _c.childProps) === null || _d === void 0 ? void 0 : _d.path) || '';
|
|
447
|
-
// Exact matches (no wildcards/params) come first
|
|
448
|
-
const aHasWildcard = pathA.includes('*') || pathA.includes(':');
|
|
449
|
-
const bHasWildcard = pathB.includes('*') || pathB.includes(':');
|
|
450
|
-
if (!aHasWildcard && bHasWildcard)
|
|
451
|
-
return -1;
|
|
452
|
-
if (aHasWildcard && !bHasWildcard)
|
|
453
|
-
return 1;
|
|
454
|
-
// Among wildcard routes, longer paths are more specific
|
|
455
|
-
return pathB.length - pathA.length;
|
|
456
|
-
});
|
|
457
|
-
};
|
|
458
|
-
|
|
459
|
-
/**
|
|
460
|
-
* `ReactRouterViewStack` is a custom navigation manager used in Ionic React
|
|
461
|
-
* apps to map React Router route elements (such as `<IonRoute>`) to "view
|
|
462
|
-
* items" that Ionic can manage in a view stack. This is critical to maintain
|
|
463
|
-
* Ionic’s animation, lifecycle, and history behavior across views.
|
|
464
|
-
*/
|
|
465
|
-
/**
|
|
466
|
-
* Delay in milliseconds before removing a Navigate view item after a redirect.
|
|
467
|
-
* This ensures the redirect navigation completes before the view is removed.
|
|
468
|
-
*/
|
|
469
|
-
const NAVIGATE_REDIRECT_DELAY_MS = 100;
|
|
470
|
-
/**
|
|
471
|
-
* Delay in milliseconds before cleaning up a view without an IonPage element.
|
|
472
|
-
* This double-checks that the view is truly not needed before removal.
|
|
473
|
-
*/
|
|
474
|
-
const VIEW_CLEANUP_DELAY_MS = 200;
|
|
475
|
-
const createDefaultMatch = (fullPathname, routeProps) => {
|
|
476
|
-
var _a, _b;
|
|
477
|
-
const isIndexRoute = !!routeProps.index;
|
|
478
|
-
const patternPath = (_a = routeProps.path) !== null && _a !== void 0 ? _a : '';
|
|
479
|
-
const pathnameBase = fullPathname === '' ? '/' : fullPathname;
|
|
480
|
-
const computedEnd = routeProps.end !== undefined ? routeProps.end : patternPath !== '' ? !patternPath.endsWith('*') : true;
|
|
481
|
-
return {
|
|
482
|
-
params: {},
|
|
483
|
-
pathname: isIndexRoute ? '' : fullPathname,
|
|
484
|
-
pathnameBase,
|
|
485
|
-
pattern: {
|
|
486
|
-
path: patternPath,
|
|
487
|
-
caseSensitive: (_b = routeProps.caseSensitive) !== null && _b !== void 0 ? _b : false,
|
|
488
|
-
end: isIndexRoute ? true : computedEnd,
|
|
489
|
-
},
|
|
490
|
-
};
|
|
491
|
-
};
|
|
492
|
-
const computeRelativeToParent = (pathname, parentPath) => {
|
|
493
|
-
if (!parentPath)
|
|
494
|
-
return null;
|
|
495
|
-
const normalizedParent = normalizePathnameForComparison(parentPath);
|
|
496
|
-
const normalizedPathname = normalizePathnameForComparison(pathname);
|
|
497
|
-
if (normalizedPathname === normalizedParent) {
|
|
498
|
-
return '';
|
|
499
|
-
}
|
|
500
|
-
const withSlash = normalizedParent === '/' ? '/' : normalizedParent + '/';
|
|
501
|
-
if (normalizedPathname.startsWith(withSlash)) {
|
|
502
|
-
return normalizedPathname.slice(withSlash.length);
|
|
503
|
-
}
|
|
504
|
-
return null;
|
|
505
|
-
};
|
|
506
|
-
const resolveIndexRouteMatch = (viewItem, pathname, parentPath) => {
|
|
507
|
-
var _a, _b, _c;
|
|
508
|
-
if (!((_b = (_a = viewItem.routeData) === null || _a === void 0 ? void 0 : _a.childProps) === null || _b === void 0 ? void 0 : _b.index)) {
|
|
509
|
-
return null;
|
|
510
|
-
}
|
|
511
|
-
// Prefer computing against the parent path when available to align with RRv6 semantics
|
|
512
|
-
const relative = computeRelativeToParent(pathname, parentPath);
|
|
513
|
-
if (relative !== null) {
|
|
514
|
-
// Index routes match only when there is no remaining path
|
|
515
|
-
if (relative === '' || relative === '/') {
|
|
516
|
-
return createDefaultMatch(parentPath || pathname, viewItem.routeData.childProps);
|
|
517
|
-
}
|
|
518
|
-
return null;
|
|
519
|
-
}
|
|
520
|
-
// Fallback: use previously computed match base for equality check
|
|
521
|
-
const previousMatch = (_c = viewItem.routeData) === null || _c === void 0 ? void 0 : _c.match;
|
|
522
|
-
if (!previousMatch) {
|
|
523
|
-
return null;
|
|
524
|
-
}
|
|
525
|
-
const normalizedPathname = normalizePathnameForComparison(pathname);
|
|
526
|
-
const normalizedBase = normalizePathnameForComparison(previousMatch.pathnameBase || previousMatch.pathname || '');
|
|
527
|
-
return normalizedPathname === normalizedBase ? previousMatch : null;
|
|
528
|
-
};
|
|
529
41
|
class ReactRouterViewStack extends ViewStacks {
|
|
530
42
|
constructor() {
|
|
531
43
|
super();
|
|
532
|
-
this.
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
const existingPath = existingRouteProps.path || '';
|
|
547
|
-
const existingElement = existingRouteProps.element;
|
|
548
|
-
const newElement = reactElement.props.element;
|
|
549
|
-
const existingIsIndexRoute = !!existingRouteProps.index;
|
|
550
|
-
const newIsIndexRoute = !!reactElement.props.index;
|
|
551
|
-
// For Navigate components, match by destination
|
|
552
|
-
const existingIsNavigate = React.isValidElement(existingElement) && existingElement.type === Navigate;
|
|
553
|
-
const newIsNavigate = React.isValidElement(newElement) && newElement.type === Navigate;
|
|
554
|
-
if (existingIsNavigate && newIsNavigate) {
|
|
555
|
-
const existingTo = (_c = existingElement.props) === null || _c === void 0 ? void 0 : _c.to;
|
|
556
|
-
const newTo = (_d = newElement.props) === null || _d === void 0 ? void 0 : _d.to;
|
|
557
|
-
if (existingTo === newTo) {
|
|
558
|
-
return true;
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
if (existingIsIndexRoute && newIsIndexRoute) {
|
|
562
|
-
return true;
|
|
563
|
-
}
|
|
564
|
-
// Reuse view items with the same path
|
|
565
|
-
// Special case: reuse tabs/* and other specific wildcard routes
|
|
566
|
-
// Don't reuse index routes (empty path) or generic catch-all wildcards (*)
|
|
567
|
-
if (existingPath === routePath && existingPath !== '' && existingPath !== '*') {
|
|
568
|
-
// Parameterized routes need pathname matching to ensure /details/1 and /details/2
|
|
569
|
-
// get separate view items. For wildcard routes (e.g., user/:userId/*), compare
|
|
570
|
-
// pathnameBase to allow child path changes while preserving the parent view.
|
|
571
|
-
const hasParams = routePath.includes(':');
|
|
572
|
-
const isWildcard = routePath.includes('*');
|
|
573
|
-
if (hasParams) {
|
|
574
|
-
if (isWildcard) {
|
|
575
|
-
const existingPathnameBase = (_f = (_e = v.routeData) === null || _e === void 0 ? void 0 : _e.match) === null || _f === void 0 ? void 0 : _f.pathnameBase;
|
|
576
|
-
const newMatch = matchComponent$1(reactElement, routeInfo.pathname, false);
|
|
577
|
-
const newPathnameBase = newMatch === null || newMatch === void 0 ? void 0 : newMatch.pathnameBase;
|
|
578
|
-
if (existingPathnameBase !== newPathnameBase) {
|
|
579
|
-
return false;
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
else {
|
|
583
|
-
const existingPathname = (_h = (_g = v.routeData) === null || _g === void 0 ? void 0 : _g.match) === null || _h === void 0 ? void 0 : _h.pathname;
|
|
584
|
-
if (existingPathname !== routeInfo.pathname) {
|
|
585
|
-
return false;
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
return true;
|
|
590
|
-
}
|
|
591
|
-
// Also reuse specific wildcard routes like tabs/*
|
|
592
|
-
if (existingPath === routePath && existingPath.endsWith('/*') && existingPath !== '/*') {
|
|
593
|
-
return true;
|
|
594
|
-
}
|
|
595
|
-
return false;
|
|
596
|
-
});
|
|
597
|
-
if (existingViewItem) {
|
|
598
|
-
// Update and ensure the existing view item is properly configured
|
|
599
|
-
existingViewItem.reactElement = reactElement;
|
|
600
|
-
existingViewItem.mount = true;
|
|
601
|
-
existingViewItem.ionPageElement = page || existingViewItem.ionPageElement;
|
|
602
|
-
const updatedMatch = matchComponent$1(reactElement, routeInfo.pathname, false) ||
|
|
603
|
-
((_a = existingViewItem.routeData) === null || _a === void 0 ? void 0 : _a.match) ||
|
|
604
|
-
createDefaultMatch(routeInfo.pathname, reactElement.props);
|
|
605
|
-
existingViewItem.routeData = {
|
|
606
|
-
match: updatedMatch,
|
|
607
|
-
childProps: reactElement.props,
|
|
608
|
-
lastPathname: (_b = existingViewItem.routeData) === null || _b === void 0 ? void 0 : _b.lastPathname, // Preserve navigation history
|
|
609
|
-
};
|
|
610
|
-
return existingViewItem;
|
|
611
|
-
}
|
|
612
|
-
this.viewItemCounter++;
|
|
613
|
-
const id = `${outletId}-${this.viewItemCounter}`;
|
|
614
|
-
const viewItem = {
|
|
615
|
-
id,
|
|
616
|
-
outletId,
|
|
617
|
-
ionPageElement: page,
|
|
618
|
-
reactElement,
|
|
619
|
-
mount: true,
|
|
620
|
-
ionRoute: true,
|
|
621
|
-
};
|
|
622
|
-
if (reactElement.type === IonRoute) {
|
|
623
|
-
viewItem.disableIonPageManagement = reactElement.props.disableIonPageManagement;
|
|
624
|
-
}
|
|
625
|
-
const initialMatch = matchComponent$1(reactElement, routeInfo.pathname, true) ||
|
|
626
|
-
createDefaultMatch(routeInfo.pathname, reactElement.props);
|
|
627
|
-
viewItem.routeData = {
|
|
628
|
-
match: initialMatch,
|
|
629
|
-
childProps: reactElement.props,
|
|
630
|
-
};
|
|
631
|
-
this.add(viewItem);
|
|
632
|
-
return viewItem;
|
|
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,
|
|
633
58
|
};
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
const routePath = viewItem.reactElement.props.path || '';
|
|
645
|
-
let match = matchComponent$1(viewItem.reactElement, routeInfo.pathname);
|
|
646
|
-
if (!match) {
|
|
647
|
-
const indexMatch = resolveIndexRouteMatch(viewItem, routeInfo.pathname, parentPath);
|
|
648
|
-
if (indexMatch) {
|
|
649
|
-
match = indexMatch;
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
// For parameterized routes, check if this is a navigation to a different path instance
|
|
653
|
-
// In that case, we should NOT reuse this view - a new view should be created
|
|
654
|
-
const isParameterRoute = routePath.includes(':');
|
|
655
|
-
const previousMatch = (_a = viewItem.routeData) === null || _a === void 0 ? void 0 : _a.match;
|
|
656
|
-
const isSamePath = (match === null || match === void 0 ? void 0 : match.pathname) === (previousMatch === null || previousMatch === void 0 ? void 0 : previousMatch.pathname);
|
|
657
|
-
// Flag to indicate this view should not be reused for this different parameterized path
|
|
658
|
-
const shouldSkipForDifferentParam = isParameterRoute && match && previousMatch && !isSamePath;
|
|
659
|
-
// Don't deactivate views automatically - let the StackManager handle view lifecycle
|
|
660
|
-
// This preserves views in the stack for navigation history like native apps
|
|
661
|
-
// Views will be hidden/shown by the StackManager's transition logic instead of being unmounted
|
|
662
|
-
// Special handling for Navigate components - they should unmount after redirecting
|
|
663
|
-
const elementComponent = (_c = (_b = viewItem.reactElement) === null || _b === void 0 ? void 0 : _b.props) === null || _c === void 0 ? void 0 : _c.element;
|
|
664
|
-
const isNavigateComponent = isNavigateElement(elementComponent);
|
|
665
|
-
if (isNavigateComponent) {
|
|
666
|
-
// Navigate components should only be mounted when they match
|
|
667
|
-
// Once they redirect (no longer match), they should be removed completely
|
|
668
|
-
// IMPORTANT: For index routes, we need to check indexMatch too since matchComponent
|
|
669
|
-
// may not properly match index routes without explicit parent path context
|
|
670
|
-
const indexMatch = ((_e = (_d = viewItem.routeData) === null || _d === void 0 ? void 0 : _d.childProps) === null || _e === void 0 ? void 0 : _e.index)
|
|
671
|
-
? resolveIndexRouteMatch(viewItem, routeInfo.pathname, parentPath)
|
|
672
|
-
: null;
|
|
673
|
-
const hasValidMatch = match || indexMatch;
|
|
674
|
-
if (!hasValidMatch && viewItem.mount) {
|
|
675
|
-
viewItem.mount = false;
|
|
676
|
-
// Schedule removal of the Navigate view item after a short delay
|
|
677
|
-
// This ensures the redirect completes before removal
|
|
678
|
-
setTimeout(() => {
|
|
679
|
-
this.remove(viewItem);
|
|
680
|
-
}, NAVIGATE_REDIRECT_DELAY_MS);
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
// Components that don't have IonPage elements and no longer match should be cleaned up
|
|
684
|
-
// BUT we need to be careful not to remove them if they're part of browser navigation history
|
|
685
|
-
// This handles components that perform immediate actions like programmatic navigation
|
|
686
|
-
// EXCEPTION: Navigate components should ALWAYS remain mounted until they redirect
|
|
687
|
-
// since they need to be rendered to trigger the navigation
|
|
688
|
-
if (!match && viewItem.mount && !viewItem.ionPageElement && !isNavigateComponent) {
|
|
689
|
-
// Check if this view item should be preserved for browser navigation
|
|
690
|
-
// We'll keep it if it was recently active (within the last navigation)
|
|
691
|
-
const shouldPreserve = viewItem.routeData.lastPathname === routeInfo.pathname ||
|
|
692
|
-
((_f = viewItem.routeData.match) === null || _f === void 0 ? void 0 : _f.pathname) === routeInfo.lastPathname;
|
|
693
|
-
if (!shouldPreserve) {
|
|
694
|
-
// This view item doesn't match and doesn't have an IonPage
|
|
695
|
-
// It's likely a utility component that performs an action and navigates away
|
|
696
|
-
viewItem.mount = false;
|
|
697
|
-
// Schedule removal to allow it to be recreated on next navigation
|
|
698
|
-
setTimeout(() => {
|
|
699
|
-
// Double-check before removing - the view might be needed again
|
|
700
|
-
const stillNotNeeded = !viewItem.mount && !viewItem.ionPageElement;
|
|
701
|
-
if (stillNotNeeded) {
|
|
702
|
-
this.remove(viewItem);
|
|
703
|
-
}
|
|
704
|
-
}, VIEW_CLEANUP_DELAY_MS);
|
|
705
|
-
}
|
|
706
|
-
else {
|
|
707
|
-
// Preserve it but unmount it for now
|
|
708
|
-
viewItem.mount = false;
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
// Reactivate view if it matches but was previously deactivated
|
|
712
|
-
// Don't reactivate if this is a parameterized route navigating to a different path instance
|
|
713
|
-
if (match && !viewItem.mount && !shouldSkipForDifferentParam) {
|
|
714
|
-
viewItem.mount = true;
|
|
715
|
-
viewItem.routeData.match = match;
|
|
716
|
-
}
|
|
717
|
-
// Deactivate wildcard routes and catch-all routes (empty path) when we have specific route matches
|
|
718
|
-
// This prevents "Not found" or fallback pages from showing alongside valid routes
|
|
719
|
-
if (routePath === '*' || routePath === '') {
|
|
720
|
-
// Check if any other view in this outlet has a match for the current route
|
|
721
|
-
const hasSpecificMatch = this.getViewItemsForOutlet(viewItem.outletId).some((v) => {
|
|
722
|
-
var _a, _b;
|
|
723
|
-
if (v.id === viewItem.id)
|
|
724
|
-
return false; // Skip self
|
|
725
|
-
const vRoutePath = ((_b = (_a = v.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path) || '';
|
|
726
|
-
if (vRoutePath === '*' || vRoutePath === '')
|
|
727
|
-
return false; // Skip other wildcard/empty routes
|
|
728
|
-
// Check if this view item would match the current route
|
|
729
|
-
const vMatch = v.reactElement ? matchComponent$1(v.reactElement, routeInfo.pathname) : null;
|
|
730
|
-
return !!vMatch;
|
|
731
|
-
});
|
|
732
|
-
if (hasSpecificMatch) {
|
|
733
|
-
viewItem.mount = false;
|
|
734
|
-
// Also hide the ion-page element immediately to prevent visual overlap
|
|
735
|
-
if (viewItem.ionPageElement) {
|
|
736
|
-
viewItem.ionPageElement.classList.add('ion-page-hidden');
|
|
737
|
-
viewItem.ionPageElement.setAttribute('aria-hidden', 'true');
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
const routeElement = React.cloneElement(viewItem.reactElement);
|
|
742
|
-
const componentElement = routeElement.props.element;
|
|
743
|
-
// Don't update match for parameterized routes navigating to different path instances
|
|
744
|
-
// This preserves the original match so that findViewItemByPath can correctly skip this view
|
|
745
|
-
if (match && viewItem.routeData.match !== match && !shouldSkipForDifferentParam) {
|
|
746
|
-
viewItem.routeData.match = match;
|
|
747
|
-
}
|
|
748
|
-
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);
|
|
749
|
-
return (React.createElement(UNSAFE_RouteContext.Consumer, { key: `view-context-${viewItem.id}` }, (parentContext) => {
|
|
750
|
-
var _a, _b, _c;
|
|
751
|
-
const parentMatches = (_a = parentContext === null || parentContext === void 0 ? void 0 : parentContext.matches) !== null && _a !== void 0 ? _a : [];
|
|
752
|
-
let accumulatedParentParams = parentMatches.reduce((acc, match) => {
|
|
753
|
-
return Object.assign(Object.assign({}, acc), match.params);
|
|
754
|
-
}, {});
|
|
755
|
-
// If parentMatches is empty, try to extract params from view items in other outlets.
|
|
756
|
-
// This handles cases where React context propagation doesn't work as expected
|
|
757
|
-
// for nested router outlets.
|
|
758
|
-
if (parentMatches.length === 0 && Object.keys(accumulatedParentParams).length === 0) {
|
|
759
|
-
const allViewItems = this.getAllViewItems();
|
|
760
|
-
for (const otherViewItem of allViewItems) {
|
|
761
|
-
// Skip view items from the same outlet
|
|
762
|
-
if (otherViewItem.outletId === viewItem.outletId)
|
|
763
|
-
continue;
|
|
764
|
-
// Check if this view item's route could match the current pathname
|
|
765
|
-
const otherMatch = (_b = otherViewItem.routeData) === null || _b === void 0 ? void 0 : _b.match;
|
|
766
|
-
if (otherMatch && otherMatch.params && Object.keys(otherMatch.params).length > 0) {
|
|
767
|
-
// Check if the current pathname starts with this view item's matched pathname
|
|
768
|
-
const matchedPathname = otherMatch.pathnameBase || otherMatch.pathname;
|
|
769
|
-
if (matchedPathname && routeInfo.pathname.startsWith(matchedPathname)) {
|
|
770
|
-
accumulatedParentParams = Object.assign(Object.assign({}, accumulatedParentParams), otherMatch.params);
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
const combinedParams = Object.assign(Object.assign({}, accumulatedParentParams), ((_c = routeMatch === null || routeMatch === void 0 ? void 0 : routeMatch.params) !== null && _c !== void 0 ? _c : {}));
|
|
776
|
-
// For relative route paths, we need to compute an absolute pathnameBase
|
|
777
|
-
// by combining the parent's pathnameBase with the matched portion
|
|
778
|
-
const routePath = routeElement.props.path;
|
|
779
|
-
const isRelativePath = routePath && !routePath.startsWith('/');
|
|
780
|
-
const isIndexRoute = !!routeElement.props.index;
|
|
781
|
-
const isSplatOnlyRoute = routePath === '*' || routePath === '/*';
|
|
782
|
-
// Get parent's pathnameBase for relative path resolution
|
|
783
|
-
const parentPathnameBase = parentMatches.length > 0 ? parentMatches[parentMatches.length - 1].pathnameBase : '/';
|
|
784
|
-
// Start with the match's pathnameBase, falling back to routeInfo.pathname
|
|
785
|
-
// BUT: splat-only routes should use parent's base (v7_relativeSplatPath behavior)
|
|
786
|
-
let absolutePathnameBase;
|
|
787
|
-
if (isSplatOnlyRoute) {
|
|
788
|
-
// Splat routes should NOT contribute their matched portion to pathnameBase
|
|
789
|
-
// This aligns with React Router v7's v7_relativeSplatPath behavior
|
|
790
|
-
// Without this, relative links inside splat routes get double path segments
|
|
791
|
-
absolutePathnameBase = parentPathnameBase;
|
|
792
|
-
}
|
|
793
|
-
else if (isRelativePath && (routeMatch === null || routeMatch === void 0 ? void 0 : routeMatch.pathnameBase)) {
|
|
794
|
-
// For relative paths with a pathnameBase, combine with parent
|
|
795
|
-
const relativeBase = routeMatch.pathnameBase.startsWith('/')
|
|
796
|
-
? routeMatch.pathnameBase.slice(1)
|
|
797
|
-
: routeMatch.pathnameBase;
|
|
798
|
-
absolutePathnameBase =
|
|
799
|
-
parentPathnameBase === '/' ? `/${relativeBase}` : `${parentPathnameBase}/${relativeBase}`;
|
|
800
|
-
}
|
|
801
|
-
else if (isIndexRoute) {
|
|
802
|
-
// Index routes should use the parent's base as their base
|
|
803
|
-
absolutePathnameBase = parentPathnameBase;
|
|
804
|
-
}
|
|
805
|
-
else {
|
|
806
|
-
// Default: use the match's pathnameBase or the current pathname
|
|
807
|
-
absolutePathnameBase = (routeMatch === null || routeMatch === void 0 ? void 0 : routeMatch.pathnameBase) || routeInfo.pathname;
|
|
808
|
-
}
|
|
809
|
-
const contextMatches = [
|
|
810
|
-
...parentMatches,
|
|
811
|
-
{
|
|
812
|
-
params: combinedParams,
|
|
813
|
-
pathname: (routeMatch === null || routeMatch === void 0 ? void 0 : routeMatch.pathname) || routeInfo.pathname,
|
|
814
|
-
pathnameBase: absolutePathnameBase,
|
|
815
|
-
route: {
|
|
816
|
-
id: viewItem.id,
|
|
817
|
-
path: routeElement.props.path,
|
|
818
|
-
element: componentElement,
|
|
819
|
-
index: !!routeElement.props.index,
|
|
820
|
-
caseSensitive: routeElement.props.caseSensitive,
|
|
821
|
-
hasErrorBoundary: false,
|
|
822
|
-
},
|
|
823
|
-
},
|
|
824
|
-
];
|
|
825
|
-
const routeContextValue = parentContext
|
|
826
|
-
? Object.assign(Object.assign({}, parentContext), { matches: contextMatches }) : {
|
|
827
|
-
outlet: null,
|
|
828
|
-
matches: contextMatches,
|
|
829
|
-
isDataRoute: false,
|
|
830
|
-
};
|
|
831
|
-
return (React.createElement(ViewLifeCycleManager, { key: `view-${viewItem.id}`, mount: viewItem.mount, removeView: () => this.remove(viewItem) },
|
|
832
|
-
React.createElement(UNSAFE_RouteContext.Provider, { value: routeContextValue }, componentElement)));
|
|
833
|
-
}));
|
|
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,
|
|
834
69
|
};
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
* Each view is wrapped in <ViewLifeCycleManager> to manage lifecycle and rendering
|
|
844
|
-
*/
|
|
845
|
-
this.getChildrenToRender = (outletId, ionRouterOutlet, routeInfo) => {
|
|
846
|
-
const viewItems = this.getViewItemsForOutlet(outletId);
|
|
847
|
-
// Determine parentPath for nested outlets to properly evaluate index routes
|
|
848
|
-
let parentPath = undefined;
|
|
849
|
-
try {
|
|
850
|
-
// Only attempt parent path computation for non-root outlets
|
|
851
|
-
// Root outlets have IDs like 'routerOutlet' or 'routerOutlet-2'
|
|
852
|
-
const isRootOutlet = outletId.startsWith('routerOutlet');
|
|
853
|
-
if (!isRootOutlet) {
|
|
854
|
-
const routeChildren = extractRouteChildren(ionRouterOutlet.props.children);
|
|
855
|
-
const { hasRelativeRoutes, hasIndexRoute, hasWildcardRoute } = analyzeRouteChildren(routeChildren);
|
|
856
|
-
if (hasRelativeRoutes || hasIndexRoute) {
|
|
857
|
-
const result = computeParentPath({
|
|
858
|
-
currentPathname: routeInfo.pathname,
|
|
859
|
-
outletMountPath: undefined,
|
|
860
|
-
routeChildren,
|
|
861
|
-
hasRelativeRoutes,
|
|
862
|
-
hasIndexRoute,
|
|
863
|
-
hasWildcardRoute,
|
|
864
|
-
});
|
|
865
|
-
parentPath = result.parentPath;
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
catch (e) {
|
|
870
|
-
// Non-fatal: if we fail to compute parentPath, fall back to previous behavior
|
|
871
|
-
}
|
|
872
|
-
// Sync child elements with stored viewItems (e.g. to reflect new props)
|
|
873
|
-
React.Children.forEach(ionRouterOutlet.props.children, (child) => {
|
|
874
|
-
// Ensure the child is a valid React element since we
|
|
875
|
-
// might have whitespace strings or other non-element children
|
|
876
|
-
if (React.isValidElement(child)) {
|
|
877
|
-
// Find view item by exact path match to avoid wildcard routes overwriting specific routes
|
|
878
|
-
const childPath = child.props.path;
|
|
879
|
-
const viewItem = viewItems.find((v) => {
|
|
880
|
-
var _a, _b;
|
|
881
|
-
const viewItemPath = (_b = (_a = v.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path;
|
|
882
|
-
// Only update if paths match exactly (prevents wildcard routes from overwriting specific routes)
|
|
883
|
-
return viewItemPath === childPath;
|
|
884
|
-
});
|
|
885
|
-
if (viewItem) {
|
|
886
|
-
viewItem.reactElement = child;
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
});
|
|
890
|
-
// Filter out duplicate view items by ID (but keep all mounted items)
|
|
891
|
-
const uniqueViewItems = viewItems.filter((viewItem, index, array) => {
|
|
892
|
-
// Remove duplicates by ID (keep first occurrence)
|
|
893
|
-
const isFirstOccurrence = array.findIndex((v) => v.id === viewItem.id) === index;
|
|
894
|
-
return isFirstOccurrence;
|
|
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);
|
|
895
78
|
});
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
const renderableViewItems = uniqueViewItems.filter((viewItem) => {
|
|
899
|
-
var _a, _b, _c, _d;
|
|
900
|
-
const elementComponent = (_b = (_a = viewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.element;
|
|
901
|
-
const isNavigateComponent = isNavigateElement(elementComponent);
|
|
902
|
-
// Exclude unmounted Navigate components from rendering
|
|
903
|
-
if (isNavigateComponent && !viewItem.mount) {
|
|
904
|
-
return false;
|
|
905
|
-
}
|
|
906
|
-
// Filter out views that are unmounted, have no ionPageElement, and don't match the current route.
|
|
907
|
-
// These are "stale" views from previous routes that should not be rendered.
|
|
908
|
-
// Views WITH ionPageElement are handled by the normal lifecycle events.
|
|
909
|
-
// Views that MATCH the current route should be kept (they might be transitioning).
|
|
910
|
-
if (!viewItem.mount && !viewItem.ionPageElement) {
|
|
911
|
-
// Check if this view's route path matches the current pathname
|
|
912
|
-
const viewRoutePath = (_d = (_c = viewItem.reactElement) === null || _c === void 0 ? void 0 : _c.props) === null || _d === void 0 ? void 0 : _d.path;
|
|
913
|
-
if (viewRoutePath) {
|
|
914
|
-
// First try exact match using matchComponent
|
|
915
|
-
const routeMatch = matchComponent$1(viewItem.reactElement, routeInfo.pathname);
|
|
916
|
-
if (routeMatch) {
|
|
917
|
-
// View matches current route, keep it
|
|
918
|
-
return true;
|
|
919
|
-
}
|
|
920
|
-
// For parent routes (like /multiple-tabs or /routing), check if current pathname
|
|
921
|
-
// starts with this route's path. This handles views with IonSplitPane/IonTabs
|
|
922
|
-
// that don't have IonPage but should remain mounted while navigating within their children.
|
|
923
|
-
const normalizedViewPath = normalizePathnameForComparison(viewRoutePath.replace(/\/?\*$/, '')); // Remove trailing wildcard
|
|
924
|
-
const normalizedCurrentPath = normalizePathnameForComparison(routeInfo.pathname);
|
|
925
|
-
// Check if current pathname is within this view's route hierarchy
|
|
926
|
-
const isWithinRouteHierarchy = normalizedCurrentPath === normalizedViewPath || normalizedCurrentPath.startsWith(normalizedViewPath + '/');
|
|
927
|
-
if (!isWithinRouteHierarchy) {
|
|
928
|
-
// View is outside current route hierarchy, remove it
|
|
929
|
-
setTimeout(() => {
|
|
930
|
-
this.remove(viewItem);
|
|
931
|
-
}, 0);
|
|
932
|
-
return false;
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
return true;
|
|
937
|
-
});
|
|
938
|
-
const renderedItems = renderableViewItems.map((viewItem) => this.renderViewItem(viewItem, routeInfo, parentPath));
|
|
939
|
-
return renderedItems;
|
|
940
|
-
};
|
|
941
|
-
/**
|
|
942
|
-
* Finds a view item matching the current route, optionally updating its match state.
|
|
943
|
-
*/
|
|
944
|
-
this.findViewItemByRouteInfo = (routeInfo, outletId, updateMatch) => {
|
|
945
|
-
const { viewItem, match } = this.findViewItemByPath(routeInfo.pathname, outletId);
|
|
946
|
-
const shouldUpdateMatch = updateMatch === undefined || updateMatch === true;
|
|
947
|
-
if (shouldUpdateMatch && viewItem && match) {
|
|
948
|
-
viewItem.routeData.match = match;
|
|
79
|
+
if (viewItem) {
|
|
80
|
+
viewItem.reactElement = child;
|
|
949
81
|
}
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
if (!routeInfo.lastPathname) {
|
|
958
|
-
return undefined;
|
|
959
|
-
}
|
|
960
|
-
const { viewItem } = this.findViewItemByPath(routeInfo.lastPathname, outletId, mustBeIonRoute);
|
|
961
|
-
return viewItem;
|
|
962
|
-
};
|
|
963
|
-
/**
|
|
964
|
-
* Finds a view item by pathname only, used in simpler queries.
|
|
965
|
-
*/
|
|
966
|
-
this.findViewItemByPathname = (pathname, outletId) => {
|
|
967
|
-
const { viewItem } = this.findViewItemByPath(pathname, outletId);
|
|
968
|
-
return viewItem;
|
|
969
|
-
};
|
|
970
|
-
/**
|
|
971
|
-
* Clean up old, unmounted view items to prevent memory leaks
|
|
972
|
-
*/
|
|
973
|
-
this.cleanupStaleViewItems = (outletId) => {
|
|
974
|
-
const viewItems = this.getViewItemsForOutlet(outletId);
|
|
975
|
-
// Keep only the most recent mounted views and a few unmounted ones for history
|
|
976
|
-
const maxUnmountedItems = 3;
|
|
977
|
-
const unmountedItems = viewItems.filter((v) => !v.mount);
|
|
978
|
-
if (unmountedItems.length > maxUnmountedItems) {
|
|
979
|
-
// Remove oldest unmounted items
|
|
980
|
-
const itemsToRemove = unmountedItems.slice(0, unmountedItems.length - maxUnmountedItems);
|
|
981
|
-
itemsToRemove.forEach((item) => {
|
|
982
|
-
this.remove(item);
|
|
983
|
-
});
|
|
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
|
+
})));
|
|
984
89
|
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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
|
+
}
|
|
994
99
|
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
100
|
+
return clonedChild;
|
|
101
|
+
});
|
|
102
|
+
return children;
|
|
103
|
+
}
|
|
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;
|
|
109
|
+
}
|
|
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;
|
|
1004
119
|
}
|
|
1005
120
|
/**
|
|
1006
|
-
*
|
|
1007
|
-
* Returns both the matched view item and match metadata.
|
|
121
|
+
* Returns the matching view item and the match result for a given pathname.
|
|
1008
122
|
*/
|
|
1009
|
-
findViewItemByPath(pathname, outletId, mustBeIonRoute
|
|
123
|
+
findViewItemByPath(pathname, outletId, mustBeIonRoute) {
|
|
1010
124
|
let viewItem;
|
|
1011
|
-
let match
|
|
125
|
+
let match;
|
|
1012
126
|
let viewStack;
|
|
1013
127
|
if (outletId) {
|
|
1014
|
-
viewStack =
|
|
128
|
+
viewStack = this.getViewItemsForOutlet(outletId);
|
|
1015
129
|
viewStack.some(matchView);
|
|
1016
|
-
if (!viewItem
|
|
130
|
+
if (!viewItem) {
|
|
1017
131
|
viewStack.some(matchDefaultRoute);
|
|
132
|
+
}
|
|
1018
133
|
}
|
|
1019
134
|
else {
|
|
1020
|
-
const viewItems =
|
|
135
|
+
const viewItems = this.getAllViewItems();
|
|
1021
136
|
viewItems.some(matchView);
|
|
1022
|
-
if (!viewItem
|
|
137
|
+
if (!viewItem) {
|
|
1023
138
|
viewItems.some(matchDefaultRoute);
|
|
139
|
+
}
|
|
1024
140
|
}
|
|
1025
|
-
// If we still have not found a view item for this outlet, try to find a matching
|
|
1026
|
-
// view item across all outlets and adopt it into the current outlet. This helps
|
|
1027
|
-
// recover when an outlet remounts and receives a new id, leaving views associated
|
|
1028
|
-
// with the previous outlet id.
|
|
1029
|
-
// Do not adopt across outlets; if we didn't find a view for this outlet,
|
|
1030
|
-
// defer to route matching to create a new one.
|
|
1031
141
|
return { viewItem, match };
|
|
1032
|
-
/**
|
|
1033
|
-
* Matches a route path with dynamic parameters (e.g. /tabs/:id)
|
|
1034
|
-
*/
|
|
1035
142
|
function matchView(v) {
|
|
1036
|
-
var _a;
|
|
1037
|
-
if (mustBeIonRoute && !v.ionRoute)
|
|
143
|
+
var _a, _b;
|
|
144
|
+
if (mustBeIonRoute && !v.ionRoute) {
|
|
1038
145
|
return false;
|
|
1039
|
-
const viewItemPath = v.routeData.childProps.path || '';
|
|
1040
|
-
const isIndexRoute = !!v.routeData.childProps.index;
|
|
1041
|
-
const previousMatch = (_a = v.routeData) === null || _a === void 0 ? void 0 : _a.match;
|
|
1042
|
-
const result = v.reactElement ? matchComponent$1(v.reactElement, pathname) : null;
|
|
1043
|
-
if (!result) {
|
|
1044
|
-
const indexMatch = resolveIndexRouteMatch(v, pathname, undefined);
|
|
1045
|
-
if (indexMatch) {
|
|
1046
|
-
match = indexMatch;
|
|
1047
|
-
viewItem = v;
|
|
1048
|
-
return true;
|
|
1049
|
-
}
|
|
1050
146
|
}
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
if (isParameterRoute && !isSamePath) {
|
|
1065
|
-
if (isWildcardRoute) {
|
|
1066
|
-
const isSameBase = result.pathnameBase === (previousMatch === null || previousMatch === void 0 ? void 0 : previousMatch.pathnameBase);
|
|
1067
|
-
if (isSameBase) {
|
|
1068
|
-
match = result;
|
|
1069
|
-
viewItem = v;
|
|
1070
|
-
return true;
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
return false;
|
|
1074
|
-
}
|
|
1075
|
-
// For routes without params, or when navigating to the exact same path,
|
|
1076
|
-
// or when there's no previous match, reuse the view item
|
|
1077
|
-
if (!hasParams || isSamePath || !previousMatch) {
|
|
1078
|
-
match = result;
|
|
1079
|
-
viewItem = v;
|
|
1080
|
-
return true;
|
|
1081
|
-
}
|
|
1082
|
-
// For wildcard routes (without params), only reuse if the pathname exactly matches
|
|
1083
|
-
if (isWildcardRoute && isSamePath) {
|
|
1084
|
-
match = result;
|
|
147
|
+
match = matchPath({
|
|
148
|
+
pathname,
|
|
149
|
+
componentProps: v.routeData.childProps,
|
|
150
|
+
});
|
|
151
|
+
if (match) {
|
|
152
|
+
/**
|
|
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.
|
|
157
|
+
*/
|
|
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))) {
|
|
1085
160
|
viewItem = v;
|
|
1086
161
|
return true;
|
|
1087
162
|
}
|
|
1088
163
|
}
|
|
1089
164
|
return false;
|
|
1090
165
|
}
|
|
1091
|
-
/**
|
|
1092
|
-
* Matches a view with no path prop (default fallback route) or index route.
|
|
1093
|
-
*/
|
|
1094
166
|
function matchDefaultRoute(v) {
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
const isDefaultRoute = childProps.path === undefined || childProps.path === '';
|
|
1098
|
-
const isIndexRoute = !!childProps.index;
|
|
1099
|
-
if (isIndexRoute) {
|
|
1100
|
-
const indexMatch = resolveIndexRouteMatch(v, pathname, undefined);
|
|
1101
|
-
if (indexMatch) {
|
|
1102
|
-
match = indexMatch;
|
|
1103
|
-
viewItem = v;
|
|
1104
|
-
return true;
|
|
1105
|
-
}
|
|
1106
|
-
return false;
|
|
1107
|
-
}
|
|
1108
|
-
// For empty path routes, only match if we're at the same level as when the view was created.
|
|
1109
|
-
// This prevents an empty path view item from being reused for different routes.
|
|
1110
|
-
if (isDefaultRoute) {
|
|
1111
|
-
const previousPathnameBase = ((_b = (_a = v.routeData) === null || _a === void 0 ? void 0 : _a.match) === null || _b === void 0 ? void 0 : _b.pathnameBase) || '';
|
|
1112
|
-
const normalizedBase = normalizePathnameForComparison(previousPathnameBase);
|
|
1113
|
-
const normalizedPathname = normalizePathnameForComparison(pathname);
|
|
1114
|
-
if (normalizedPathname !== normalizedBase) {
|
|
1115
|
-
return false;
|
|
1116
|
-
}
|
|
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) {
|
|
1117
169
|
match = {
|
|
170
|
+
path: pathname,
|
|
171
|
+
url: pathname,
|
|
172
|
+
isExact: true,
|
|
1118
173
|
params: {},
|
|
1119
|
-
pathname,
|
|
1120
|
-
pathnameBase: pathname === '' ? '/' : pathname,
|
|
1121
|
-
pattern: {
|
|
1122
|
-
path: '',
|
|
1123
|
-
caseSensitive: (_c = childProps.caseSensitive) !== null && _c !== void 0 ? _c : false,
|
|
1124
|
-
end: true,
|
|
1125
|
-
},
|
|
1126
174
|
};
|
|
1127
175
|
viewItem = v;
|
|
1128
176
|
return true;
|
|
@@ -1131,29 +179,11 @@ class ReactRouterViewStack extends ViewStacks {
|
|
|
1131
179
|
}
|
|
1132
180
|
}
|
|
1133
181
|
}
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
var _a;
|
|
1139
|
-
const routeProps = (_a = node === null || node === void 0 ? void 0 : node.props) !== null && _a !== void 0 ? _a : {};
|
|
1140
|
-
const routePath = routeProps.path;
|
|
1141
|
-
const pathnameToMatch = derivePathnameToMatch(pathname, routePath);
|
|
1142
|
-
const match = matchPath({
|
|
1143
|
-
pathname: pathnameToMatch,
|
|
1144
|
-
componentProps: routeProps,
|
|
182
|
+
function matchComponent$1(node, pathname) {
|
|
183
|
+
return matchPath({
|
|
184
|
+
pathname,
|
|
185
|
+
componentProps: node.props,
|
|
1145
186
|
});
|
|
1146
|
-
if (match || !allowFallback) {
|
|
1147
|
-
return match;
|
|
1148
|
-
}
|
|
1149
|
-
const isIndexRoute = !!routeProps.index;
|
|
1150
|
-
if (isIndexRoute) {
|
|
1151
|
-
return createDefaultMatch(pathname, routeProps);
|
|
1152
|
-
}
|
|
1153
|
-
if (!routePath || routePath === '') {
|
|
1154
|
-
return createDefaultMatch(pathname, routeProps);
|
|
1155
|
-
}
|
|
1156
|
-
return null;
|
|
1157
187
|
}
|
|
1158
188
|
|
|
1159
189
|
function clonePageElement(leavingViewHtml) {
|
|
@@ -1178,40 +208,7 @@ function clonePageElement(leavingViewHtml) {
|
|
|
1178
208
|
return undefined;
|
|
1179
209
|
}
|
|
1180
210
|
|
|
1181
|
-
/**
|
|
1182
|
-
* `StackManager` is responsible for managing page transitions, keeping track
|
|
1183
|
-
* of views (pages), and ensuring that navigation behaves like native apps —
|
|
1184
|
-
* particularly with animations and swipe gestures.
|
|
1185
|
-
*/
|
|
1186
|
-
/**
|
|
1187
|
-
* Delay in milliseconds before unmounting a view after a transition completes.
|
|
1188
|
-
* This ensures the page transition animation finishes before the view is removed.
|
|
1189
|
-
*/
|
|
1190
|
-
const VIEW_UNMOUNT_DELAY_MS = 250;
|
|
1191
|
-
/**
|
|
1192
|
-
* Delay in milliseconds to wait for an IonPage element to be mounted before
|
|
1193
|
-
* proceeding with a page transition.
|
|
1194
|
-
*/
|
|
1195
|
-
const ION_PAGE_WAIT_TIMEOUT_MS = 50;
|
|
1196
211
|
const isViewVisible = (el) => !el.classList.contains('ion-page-invisible') && !el.classList.contains('ion-page-hidden');
|
|
1197
|
-
/**
|
|
1198
|
-
* Hides an ion-page element by adding hidden class and aria attribute.
|
|
1199
|
-
*/
|
|
1200
|
-
const hideIonPageElement = (element) => {
|
|
1201
|
-
if (element) {
|
|
1202
|
-
element.classList.add('ion-page-hidden');
|
|
1203
|
-
element.setAttribute('aria-hidden', 'true');
|
|
1204
|
-
}
|
|
1205
|
-
};
|
|
1206
|
-
/**
|
|
1207
|
-
* Shows an ion-page element by removing hidden class and aria attribute.
|
|
1208
|
-
*/
|
|
1209
|
-
const showIonPageElement = (element) => {
|
|
1210
|
-
if (element) {
|
|
1211
|
-
element.classList.remove('ion-page-hidden');
|
|
1212
|
-
element.removeAttribute('aria-hidden');
|
|
1213
|
-
}
|
|
1214
|
-
};
|
|
1215
212
|
class StackManager extends React.PureComponent {
|
|
1216
213
|
constructor(props) {
|
|
1217
214
|
super(props);
|
|
@@ -1220,420 +217,13 @@ class StackManager extends React.PureComponent {
|
|
|
1220
217
|
isInOutlet: () => true,
|
|
1221
218
|
};
|
|
1222
219
|
this.pendingPageTransition = false;
|
|
1223
|
-
this.waitingForIonPage = false;
|
|
1224
|
-
this.outletMountPath = undefined;
|
|
1225
220
|
this.registerIonPage = this.registerIonPage.bind(this);
|
|
1226
221
|
this.transitionPage = this.transitionPage.bind(this);
|
|
1227
222
|
this.handlePageTransition = this.handlePageTransition.bind(this);
|
|
1228
|
-
this.id =
|
|
223
|
+
this.id = generateId('routerOutlet');
|
|
1229
224
|
this.prevProps = undefined;
|
|
1230
225
|
this.skipTransition = false;
|
|
1231
226
|
}
|
|
1232
|
-
/**
|
|
1233
|
-
* Determines the parent path that was matched to reach this outlet.
|
|
1234
|
-
* This helps with nested routing in React Router 6.
|
|
1235
|
-
*
|
|
1236
|
-
* The algorithm finds the shortest parent path where a route matches the remaining path.
|
|
1237
|
-
* Priority: specific routes > wildcard routes > index routes (only at mount point)
|
|
1238
|
-
*/
|
|
1239
|
-
getParentPath() {
|
|
1240
|
-
const currentPathname = this.props.routeInfo.pathname;
|
|
1241
|
-
// If this outlet previously established a mount path and the current
|
|
1242
|
-
// pathname is outside of that scope, do not attempt to re-compute a new
|
|
1243
|
-
// parent path. This prevents out-of-scope outlets from "adopting"
|
|
1244
|
-
// unrelated routes (e.g., matching their index route under /overlays).
|
|
1245
|
-
if (this.outletMountPath && !currentPathname.startsWith(this.outletMountPath)) {
|
|
1246
|
-
return undefined;
|
|
1247
|
-
}
|
|
1248
|
-
// Check if this outlet has route children to analyze
|
|
1249
|
-
if (this.ionRouterOutlet) {
|
|
1250
|
-
const routeChildren = extractRouteChildren(this.ionRouterOutlet.props.children);
|
|
1251
|
-
const { hasRelativeRoutes, hasIndexRoute, hasWildcardRoute } = analyzeRouteChildren(routeChildren);
|
|
1252
|
-
// Root outlets have IDs like 'routerOutlet' or 'routerOutlet-2'
|
|
1253
|
-
// But even outlets with auto-generated IDs may need parent path computation
|
|
1254
|
-
// if they have relative routes (indicating they're nested outlets)
|
|
1255
|
-
const isRootOutlet = this.id.startsWith('routerOutlet');
|
|
1256
|
-
const needsParentPath = !isRootOutlet || hasRelativeRoutes || hasIndexRoute;
|
|
1257
|
-
if (needsParentPath) {
|
|
1258
|
-
const result = computeParentPath({
|
|
1259
|
-
currentPathname,
|
|
1260
|
-
outletMountPath: this.outletMountPath,
|
|
1261
|
-
routeChildren,
|
|
1262
|
-
hasRelativeRoutes,
|
|
1263
|
-
hasIndexRoute,
|
|
1264
|
-
hasWildcardRoute,
|
|
1265
|
-
});
|
|
1266
|
-
// Update the outlet mount path if it was set
|
|
1267
|
-
if (result.outletMountPath && !this.outletMountPath) {
|
|
1268
|
-
this.outletMountPath = result.outletMountPath;
|
|
1269
|
-
}
|
|
1270
|
-
return result.parentPath;
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
return this.outletMountPath;
|
|
1274
|
-
}
|
|
1275
|
-
/**
|
|
1276
|
-
* Finds the entering and leaving view items for a route transition,
|
|
1277
|
-
* handling special redirect cases.
|
|
1278
|
-
*/
|
|
1279
|
-
findViewItems(routeInfo) {
|
|
1280
|
-
const enteringViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id);
|
|
1281
|
-
let leavingViewItem = this.context.findLeavingViewItemByRouteInfo(routeInfo, this.id);
|
|
1282
|
-
// If we don't have a leaving view item, but the route info indicates
|
|
1283
|
-
// that the user has routed from a previous path, then the leaving view
|
|
1284
|
-
// can be found by the last known pathname.
|
|
1285
|
-
if (!leavingViewItem && routeInfo.prevRouteLastPathname) {
|
|
1286
|
-
leavingViewItem = this.context.findViewItemByPathname(routeInfo.prevRouteLastPathname, this.id);
|
|
1287
|
-
}
|
|
1288
|
-
// Special case for redirects: When a redirect happens inside a nested route,
|
|
1289
|
-
// the entering and leaving view might be the same (the container route like tabs/*).
|
|
1290
|
-
// In this case, we need to look at prevRouteLastPathname to find the actual
|
|
1291
|
-
// view we're transitioning away from.
|
|
1292
|
-
if (enteringViewItem &&
|
|
1293
|
-
leavingViewItem &&
|
|
1294
|
-
enteringViewItem === leavingViewItem &&
|
|
1295
|
-
routeInfo.routeAction === 'replace' &&
|
|
1296
|
-
routeInfo.prevRouteLastPathname) {
|
|
1297
|
-
const actualLeavingView = this.context.findViewItemByPathname(routeInfo.prevRouteLastPathname, this.id);
|
|
1298
|
-
if (actualLeavingView && actualLeavingView !== enteringViewItem) {
|
|
1299
|
-
leavingViewItem = actualLeavingView;
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
// Also check if we're in a redirect scenario where entering and leaving are different
|
|
1303
|
-
// but we still need to handle the actual previous view.
|
|
1304
|
-
if (enteringViewItem &&
|
|
1305
|
-
!leavingViewItem &&
|
|
1306
|
-
routeInfo.routeAction === 'replace' &&
|
|
1307
|
-
routeInfo.prevRouteLastPathname) {
|
|
1308
|
-
const actualLeavingView = this.context.findViewItemByPathname(routeInfo.prevRouteLastPathname, this.id);
|
|
1309
|
-
if (actualLeavingView && actualLeavingView !== enteringViewItem) {
|
|
1310
|
-
leavingViewItem = actualLeavingView;
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
return { enteringViewItem, leavingViewItem };
|
|
1314
|
-
}
|
|
1315
|
-
/**
|
|
1316
|
-
* Determines if the leaving view item should be unmounted after a transition.
|
|
1317
|
-
*/
|
|
1318
|
-
shouldUnmountLeavingView(routeInfo, enteringViewItem, leavingViewItem) {
|
|
1319
|
-
var _a, _b, _c, _d;
|
|
1320
|
-
if (!leavingViewItem) {
|
|
1321
|
-
return false;
|
|
1322
|
-
}
|
|
1323
|
-
if (routeInfo.routeAction === 'replace') {
|
|
1324
|
-
// For replace actions, decide whether to unmount the leaving view.
|
|
1325
|
-
// The key question is: are these routes in the same navigation context?
|
|
1326
|
-
const enteringRoutePath = (_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;
|
|
1327
|
-
const leavingRoutePath = (_d = (_c = leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.reactElement) === null || _c === void 0 ? void 0 : _c.props) === null || _d === void 0 ? void 0 : _d.path;
|
|
1328
|
-
// Never unmount the root path "/" - it's the main entry point for back navigation
|
|
1329
|
-
if (leavingRoutePath === '/' || leavingRoutePath === '') {
|
|
1330
|
-
return false;
|
|
1331
|
-
}
|
|
1332
|
-
if (enteringRoutePath && leavingRoutePath) {
|
|
1333
|
-
// Get parent paths to check if routes share a common parent
|
|
1334
|
-
const getParentPath = (path) => {
|
|
1335
|
-
const normalized = path.replace(/\/\*$/, ''); // Remove trailing /*
|
|
1336
|
-
const lastSlash = normalized.lastIndexOf('/');
|
|
1337
|
-
return lastSlash > 0 ? normalized.substring(0, lastSlash) : '/';
|
|
1338
|
-
};
|
|
1339
|
-
const enteringParent = getParentPath(enteringRoutePath);
|
|
1340
|
-
const leavingParent = getParentPath(leavingRoutePath);
|
|
1341
|
-
// Unmount if:
|
|
1342
|
-
// 1. Routes are siblings (same parent, e.g., /page1 and /page2, or /foo/page1 and /foo/page2)
|
|
1343
|
-
// 2. Entering is a child of leaving (redirect, e.g., /tabs -> /tabs/tab1)
|
|
1344
|
-
const areSiblings = enteringParent === leavingParent && enteringParent !== '/';
|
|
1345
|
-
const isChildRedirect = enteringRoutePath.startsWith(leavingRoutePath) ||
|
|
1346
|
-
(leavingRoutePath.endsWith('/*') && enteringRoutePath.startsWith(leavingRoutePath.slice(0, -2)));
|
|
1347
|
-
return areSiblings || isChildRedirect;
|
|
1348
|
-
}
|
|
1349
|
-
return false;
|
|
1350
|
-
}
|
|
1351
|
-
// For non-replace actions, only unmount for back navigation (not forward push)
|
|
1352
|
-
const isForwardPush = routeInfo.routeAction === 'push' && routeInfo.routeDirection === 'forward';
|
|
1353
|
-
if (!isForwardPush && routeInfo.routeDirection !== 'none' && enteringViewItem !== leavingViewItem) {
|
|
1354
|
-
return true;
|
|
1355
|
-
}
|
|
1356
|
-
return false;
|
|
1357
|
-
}
|
|
1358
|
-
/**
|
|
1359
|
-
* Handles the case when the outlet is out of scope (current route is outside mount path).
|
|
1360
|
-
* Returns true if the transition should be aborted.
|
|
1361
|
-
*/
|
|
1362
|
-
handleOutOfScopeOutlet(routeInfo) {
|
|
1363
|
-
if (!this.outletMountPath || routeInfo.pathname.startsWith(this.outletMountPath)) {
|
|
1364
|
-
return false;
|
|
1365
|
-
}
|
|
1366
|
-
// Clear any pending unmount timeout to avoid conflicts
|
|
1367
|
-
if (this.outOfScopeUnmountTimeout) {
|
|
1368
|
-
clearTimeout(this.outOfScopeUnmountTimeout);
|
|
1369
|
-
this.outOfScopeUnmountTimeout = undefined;
|
|
1370
|
-
}
|
|
1371
|
-
// When an outlet is out of scope, unmount its views immediately
|
|
1372
|
-
const allViewsInOutlet = this.context.getViewItemsForOutlet ? this.context.getViewItemsForOutlet(this.id) : [];
|
|
1373
|
-
// Unmount and remove all views in this outlet immediately to avoid leftover content
|
|
1374
|
-
allViewsInOutlet.forEach((viewItem) => {
|
|
1375
|
-
hideIonPageElement(viewItem.ionPageElement);
|
|
1376
|
-
this.context.unMountViewItem(viewItem);
|
|
1377
|
-
});
|
|
1378
|
-
this.forceUpdate();
|
|
1379
|
-
return true;
|
|
1380
|
-
}
|
|
1381
|
-
/**
|
|
1382
|
-
* Handles the case when this is a nested outlet with relative routes but no valid parent path.
|
|
1383
|
-
* Returns true if the transition should be aborted.
|
|
1384
|
-
*/
|
|
1385
|
-
handleOutOfContextNestedOutlet(parentPath, leavingViewItem) {
|
|
1386
|
-
var _a;
|
|
1387
|
-
// Root outlets have IDs like 'routerOutlet' or 'routerOutlet-2'
|
|
1388
|
-
const isRootOutlet = this.id.startsWith('routerOutlet');
|
|
1389
|
-
if (isRootOutlet || parentPath !== undefined || !this.ionRouterOutlet) {
|
|
1390
|
-
return false;
|
|
1391
|
-
}
|
|
1392
|
-
const routesChildren = (_a = getRoutesChildren(this.ionRouterOutlet.props.children)) !== null && _a !== void 0 ? _a : this.ionRouterOutlet.props.children;
|
|
1393
|
-
const routeChildren = React.Children.toArray(routesChildren).filter((child) => React.isValidElement(child) && child.type === Route);
|
|
1394
|
-
const hasRelativeRoutes = routeChildren.some((route) => {
|
|
1395
|
-
const path = route.props.path;
|
|
1396
|
-
return path && !path.startsWith('/') && path !== '*';
|
|
1397
|
-
});
|
|
1398
|
-
if (hasRelativeRoutes) {
|
|
1399
|
-
// Hide any visible views in this outlet since it's out of scope
|
|
1400
|
-
hideIonPageElement(leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.ionPageElement);
|
|
1401
|
-
if (leavingViewItem) {
|
|
1402
|
-
leavingViewItem.mount = false;
|
|
1403
|
-
}
|
|
1404
|
-
this.forceUpdate();
|
|
1405
|
-
return true;
|
|
1406
|
-
}
|
|
1407
|
-
return false;
|
|
1408
|
-
}
|
|
1409
|
-
/**
|
|
1410
|
-
* Handles the case when a nested outlet has no matching route.
|
|
1411
|
-
* Returns true if the transition should be aborted.
|
|
1412
|
-
*/
|
|
1413
|
-
handleNoMatchingRoute(enteringRoute, enteringViewItem, leavingViewItem) {
|
|
1414
|
-
// Root outlets have IDs like 'routerOutlet' or 'routerOutlet-2'
|
|
1415
|
-
const isRootOutlet = this.id.startsWith('routerOutlet');
|
|
1416
|
-
if (isRootOutlet || enteringRoute || enteringViewItem) {
|
|
1417
|
-
return false;
|
|
1418
|
-
}
|
|
1419
|
-
// Hide any visible views in this outlet since it has no matching route
|
|
1420
|
-
hideIonPageElement(leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.ionPageElement);
|
|
1421
|
-
if (leavingViewItem) {
|
|
1422
|
-
leavingViewItem.mount = false;
|
|
1423
|
-
}
|
|
1424
|
-
this.forceUpdate();
|
|
1425
|
-
return true;
|
|
1426
|
-
}
|
|
1427
|
-
/**
|
|
1428
|
-
* Handles the transition when entering view item has an ion-page element ready.
|
|
1429
|
-
*/
|
|
1430
|
-
handleReadyEnteringView(routeInfo, enteringViewItem, leavingViewItem, shouldUnmountLeavingViewItem) {
|
|
1431
|
-
var _a, _b;
|
|
1432
|
-
// Handle same view item case (e.g., parameterized route changes)
|
|
1433
|
-
if (enteringViewItem === leavingViewItem) {
|
|
1434
|
-
const routePath = (_b = (_a = enteringViewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path;
|
|
1435
|
-
const isParameterizedRoute = routePath ? routePath.includes(':') : false;
|
|
1436
|
-
if (isParameterizedRoute) {
|
|
1437
|
-
// Refresh match metadata so the component receives updated params
|
|
1438
|
-
const updatedMatch = matchComponent(enteringViewItem.reactElement, routeInfo.pathname, true);
|
|
1439
|
-
if (updatedMatch) {
|
|
1440
|
-
enteringViewItem.routeData.match = updatedMatch;
|
|
1441
|
-
}
|
|
1442
|
-
const enteringEl = enteringViewItem.ionPageElement;
|
|
1443
|
-
if (enteringEl) {
|
|
1444
|
-
enteringEl.classList.remove('ion-page-hidden', 'ion-page-invisible');
|
|
1445
|
-
enteringEl.removeAttribute('aria-hidden');
|
|
1446
|
-
}
|
|
1447
|
-
this.forceUpdate();
|
|
1448
|
-
return;
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
// Try to find leaving view using prev route info if still not found
|
|
1452
|
-
if (!leavingViewItem && this.props.routeInfo.prevRouteLastPathname) {
|
|
1453
|
-
leavingViewItem = this.context.findViewItemByPathname(this.props.routeInfo.prevRouteLastPathname, this.id);
|
|
1454
|
-
}
|
|
1455
|
-
// Ensure the entering view is marked as mounted.
|
|
1456
|
-
// This is critical for views that were previously unmounted (e.g., navigating back to home).
|
|
1457
|
-
// When mount=false, the ViewLifeCycleManager doesn't render the IonPage, so the
|
|
1458
|
-
// ionPageElement reference becomes stale. By setting mount=true, we ensure the view
|
|
1459
|
-
// gets re-rendered and a new IonPage is created.
|
|
1460
|
-
if (!enteringViewItem.mount) {
|
|
1461
|
-
enteringViewItem.mount = true;
|
|
1462
|
-
}
|
|
1463
|
-
// Check visibility state BEFORE showing the entering view.
|
|
1464
|
-
// This must be done before showIonPageElement to get accurate visibility state.
|
|
1465
|
-
const enteringWasVisible = enteringViewItem.ionPageElement && isViewVisible(enteringViewItem.ionPageElement);
|
|
1466
|
-
const leavingIsHidden = leavingViewItem !== undefined && leavingViewItem.ionPageElement && !isViewVisible(leavingViewItem.ionPageElement);
|
|
1467
|
-
// Check for duplicate transition
|
|
1468
|
-
const currentTransition = {
|
|
1469
|
-
enteringId: enteringViewItem.id,
|
|
1470
|
-
leavingId: leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.id,
|
|
1471
|
-
};
|
|
1472
|
-
const isDuplicateTransition = leavingViewItem &&
|
|
1473
|
-
this.lastTransition &&
|
|
1474
|
-
this.lastTransition.leavingId &&
|
|
1475
|
-
this.lastTransition.enteringId === currentTransition.enteringId &&
|
|
1476
|
-
this.lastTransition.leavingId === currentTransition.leavingId;
|
|
1477
|
-
// Skip transition if entering view was ALREADY visible and leaving view is not visible.
|
|
1478
|
-
// This indicates the transition has already been performed (e.g., via swipe gesture).
|
|
1479
|
-
// IMPORTANT: Only skip if both ionPageElements are the same as when the transition was last done.
|
|
1480
|
-
// If the leaving view's ionPageElement changed (e.g., component re-rendered with different IonPage),
|
|
1481
|
-
// we should NOT skip because the DOM state is inconsistent.
|
|
1482
|
-
if (enteringWasVisible && leavingIsHidden && isDuplicateTransition) {
|
|
1483
|
-
// For swipe-to-go-back, the transition animation was handled by the gesture.
|
|
1484
|
-
// We still need to set mount=false so React unmounts the leaving view.
|
|
1485
|
-
// Only do this when skipTransition is set (indicating gesture completion).
|
|
1486
|
-
if (this.skipTransition &&
|
|
1487
|
-
shouldUnmountLeavingViewItem &&
|
|
1488
|
-
leavingViewItem &&
|
|
1489
|
-
enteringViewItem !== leavingViewItem) {
|
|
1490
|
-
leavingViewItem.mount = false;
|
|
1491
|
-
// Call transitionPage with duration 0 to trigger ionViewDidLeave lifecycle
|
|
1492
|
-
// which is needed for ViewLifeCycleManager to remove the view.
|
|
1493
|
-
this.transitionPage(routeInfo, enteringViewItem, leavingViewItem, 'back');
|
|
1494
|
-
}
|
|
1495
|
-
// Clear skipTransition since we're not calling transitionPage which normally clears it
|
|
1496
|
-
this.skipTransition = false;
|
|
1497
|
-
// Must call forceUpdate to trigger re-render after mount state change
|
|
1498
|
-
this.forceUpdate();
|
|
1499
|
-
return;
|
|
1500
|
-
}
|
|
1501
|
-
// Ensure the entering view is not hidden from previous navigations
|
|
1502
|
-
// This must happen AFTER the visibility check above
|
|
1503
|
-
showIonPageElement(enteringViewItem.ionPageElement);
|
|
1504
|
-
// Skip if this is a duplicate transition (but visibility state didn't match above)
|
|
1505
|
-
// OR if skipTransition is set (swipe gesture already handled the animation)
|
|
1506
|
-
if (isDuplicateTransition || this.skipTransition) {
|
|
1507
|
-
// For swipe-to-go-back, we still need to handle unmounting even if visibility
|
|
1508
|
-
// conditions aren't fully met (animation might still be in progress)
|
|
1509
|
-
if (this.skipTransition &&
|
|
1510
|
-
shouldUnmountLeavingViewItem &&
|
|
1511
|
-
leavingViewItem &&
|
|
1512
|
-
enteringViewItem !== leavingViewItem) {
|
|
1513
|
-
leavingViewItem.mount = false;
|
|
1514
|
-
// For swipe-to-go-back, we need to call transitionPage with duration 0 to
|
|
1515
|
-
// trigger the ionViewDidLeave lifecycle event. The ViewLifeCycleManager
|
|
1516
|
-
// uses componentCanBeDestroyed callback to remove the view, which is
|
|
1517
|
-
// only called from ionViewDidLeave. Since the gesture animation already
|
|
1518
|
-
// completed before mount=false was set, we need to re-fire the lifecycle.
|
|
1519
|
-
this.transitionPage(routeInfo, enteringViewItem, leavingViewItem, 'back');
|
|
1520
|
-
}
|
|
1521
|
-
// Clear skipTransition since we're not calling transitionPage which normally clears it
|
|
1522
|
-
this.skipTransition = false;
|
|
1523
|
-
// Must call forceUpdate to trigger re-render after mount state change
|
|
1524
|
-
this.forceUpdate();
|
|
1525
|
-
return;
|
|
1526
|
-
}
|
|
1527
|
-
this.lastTransition = currentTransition;
|
|
1528
|
-
this.transitionPage(routeInfo, enteringViewItem, leavingViewItem);
|
|
1529
|
-
// Handle unmounting the leaving view
|
|
1530
|
-
if (shouldUnmountLeavingViewItem && leavingViewItem && enteringViewItem !== leavingViewItem) {
|
|
1531
|
-
leavingViewItem.mount = false;
|
|
1532
|
-
this.handleLeavingViewUnmount(routeInfo, enteringViewItem, leavingViewItem);
|
|
1533
|
-
}
|
|
1534
|
-
}
|
|
1535
|
-
/**
|
|
1536
|
-
* Handles the delayed unmount of the leaving view item.
|
|
1537
|
-
* For 'replace' actions: handles container route transitions specially.
|
|
1538
|
-
* For back navigation: explicitly unmounts because the ionViewDidLeave lifecycle
|
|
1539
|
-
* fires DURING transitionPage, but mount=false is set AFTER.
|
|
1540
|
-
*
|
|
1541
|
-
* @param routeInfo Current route information
|
|
1542
|
-
* @param enteringViewItem The view being navigated to
|
|
1543
|
-
* @param leavingViewItem The view being navigated from
|
|
1544
|
-
*/
|
|
1545
|
-
handleLeavingViewUnmount(routeInfo, enteringViewItem, leavingViewItem) {
|
|
1546
|
-
var _a, _b, _c, _d, _e, _f;
|
|
1547
|
-
if (!leavingViewItem.ionPageElement) {
|
|
1548
|
-
return;
|
|
1549
|
-
}
|
|
1550
|
-
// For push/pop actions, do NOT unmount - views are cached for navigation history.
|
|
1551
|
-
// Push: Forward navigation caches views for back navigation
|
|
1552
|
-
// Pop: Back navigation should not unmount the entering view's history
|
|
1553
|
-
// Only 'replace' actions should actually unmount views since they replace history.
|
|
1554
|
-
if (routeInfo.routeAction !== 'replace') {
|
|
1555
|
-
return;
|
|
1556
|
-
}
|
|
1557
|
-
// For replace actions, check if we should skip removal for nested outlet redirects
|
|
1558
|
-
const enteringRoutePath = (_b = (_a = enteringViewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path;
|
|
1559
|
-
const leavingRoutePath = (_d = (_c = leavingViewItem.reactElement) === null || _c === void 0 ? void 0 : _c.props) === null || _d === void 0 ? void 0 : _d.path;
|
|
1560
|
-
const isEnteringContainerRoute = enteringRoutePath && enteringRoutePath.endsWith('/*');
|
|
1561
|
-
const isLeavingSpecificRoute = leavingRoutePath &&
|
|
1562
|
-
leavingRoutePath !== '' &&
|
|
1563
|
-
leavingRoutePath !== '*' &&
|
|
1564
|
-
!leavingRoutePath.endsWith('/*') &&
|
|
1565
|
-
!((_f = (_e = leavingViewItem.reactElement) === null || _e === void 0 ? void 0 : _e.props) === null || _f === void 0 ? void 0 : _f.index);
|
|
1566
|
-
// Skip removal only for container-to-container transitions
|
|
1567
|
-
if (isEnteringContainerRoute && !isLeavingSpecificRoute) {
|
|
1568
|
-
return;
|
|
1569
|
-
}
|
|
1570
|
-
const viewToUnmount = leavingViewItem;
|
|
1571
|
-
setTimeout(() => {
|
|
1572
|
-
this.context.unMountViewItem(viewToUnmount);
|
|
1573
|
-
// Trigger re-render to remove the view from DOM
|
|
1574
|
-
this.forceUpdate();
|
|
1575
|
-
}, VIEW_UNMOUNT_DELAY_MS);
|
|
1576
|
-
}
|
|
1577
|
-
/**
|
|
1578
|
-
* Handles the case when entering view has no ion-page element yet (waiting for render).
|
|
1579
|
-
*/
|
|
1580
|
-
handleWaitingForIonPage(routeInfo, enteringViewItem, leavingViewItem, shouldUnmountLeavingViewItem) {
|
|
1581
|
-
var _a, _b;
|
|
1582
|
-
const enteringRouteElement = (_b = (_a = enteringViewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.element;
|
|
1583
|
-
// Handle Navigate components (they never render an IonPage)
|
|
1584
|
-
if (isNavigateElement(enteringRouteElement)) {
|
|
1585
|
-
this.waitingForIonPage = false;
|
|
1586
|
-
if (this.ionPageWaitTimeout) {
|
|
1587
|
-
clearTimeout(this.ionPageWaitTimeout);
|
|
1588
|
-
this.ionPageWaitTimeout = undefined;
|
|
1589
|
-
}
|
|
1590
|
-
this.pendingPageTransition = false;
|
|
1591
|
-
// Hide the leaving view immediately for Navigate redirects
|
|
1592
|
-
hideIonPageElement(leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.ionPageElement);
|
|
1593
|
-
// Don't unmount if entering and leaving are the same view item
|
|
1594
|
-
if (shouldUnmountLeavingViewItem && leavingViewItem && enteringViewItem !== leavingViewItem) {
|
|
1595
|
-
leavingViewItem.mount = false;
|
|
1596
|
-
}
|
|
1597
|
-
this.forceUpdate();
|
|
1598
|
-
return;
|
|
1599
|
-
}
|
|
1600
|
-
// Hide leaving view while we wait for the entering view's IonPage to mount
|
|
1601
|
-
hideIonPageElement(leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.ionPageElement);
|
|
1602
|
-
this.waitingForIonPage = true;
|
|
1603
|
-
if (this.ionPageWaitTimeout) {
|
|
1604
|
-
clearTimeout(this.ionPageWaitTimeout);
|
|
1605
|
-
}
|
|
1606
|
-
this.ionPageWaitTimeout = setTimeout(() => {
|
|
1607
|
-
var _a, _b;
|
|
1608
|
-
this.ionPageWaitTimeout = undefined;
|
|
1609
|
-
if (!this.waitingForIonPage) {
|
|
1610
|
-
return;
|
|
1611
|
-
}
|
|
1612
|
-
this.waitingForIonPage = false;
|
|
1613
|
-
const latestEnteringView = (_a = this.context.findViewItemByRouteInfo(routeInfo, this.id)) !== null && _a !== void 0 ? _a : enteringViewItem;
|
|
1614
|
-
const latestLeavingView = (_b = this.context.findLeavingViewItemByRouteInfo(routeInfo, this.id)) !== null && _b !== void 0 ? _b : leavingViewItem;
|
|
1615
|
-
if (latestEnteringView === null || latestEnteringView === void 0 ? void 0 : latestEnteringView.ionPageElement) {
|
|
1616
|
-
this.transitionPage(routeInfo, latestEnteringView, latestLeavingView !== null && latestLeavingView !== void 0 ? latestLeavingView : undefined);
|
|
1617
|
-
if (shouldUnmountLeavingViewItem && latestLeavingView && latestEnteringView !== latestLeavingView) {
|
|
1618
|
-
latestLeavingView.mount = false;
|
|
1619
|
-
// Call handleLeavingViewUnmount to ensure the view is properly removed
|
|
1620
|
-
this.handleLeavingViewUnmount(routeInfo, latestEnteringView, latestLeavingView);
|
|
1621
|
-
}
|
|
1622
|
-
this.forceUpdate();
|
|
1623
|
-
}
|
|
1624
|
-
}, ION_PAGE_WAIT_TIMEOUT_MS);
|
|
1625
|
-
this.forceUpdate();
|
|
1626
|
-
}
|
|
1627
|
-
/**
|
|
1628
|
-
* Gets the route info to use for finding views during swipe-to-go-back gestures.
|
|
1629
|
-
* This pattern is used in multiple places in setupRouterOutlet.
|
|
1630
|
-
*/
|
|
1631
|
-
getSwipeBackRouteInfo() {
|
|
1632
|
-
const { routeInfo } = this.props;
|
|
1633
|
-
return this.prevProps && this.prevProps.routeInfo.pathname === routeInfo.pushedByRoute
|
|
1634
|
-
? this.prevProps.routeInfo
|
|
1635
|
-
: { pathname: routeInfo.pushedByRoute || '' };
|
|
1636
|
-
}
|
|
1637
227
|
componentDidMount() {
|
|
1638
228
|
if (this.clearOutletTimeout) {
|
|
1639
229
|
/**
|
|
@@ -1665,153 +255,117 @@ class StackManager extends React.PureComponent {
|
|
|
1665
255
|
}
|
|
1666
256
|
}
|
|
1667
257
|
componentWillUnmount() {
|
|
1668
|
-
if (this.ionPageWaitTimeout) {
|
|
1669
|
-
clearTimeout(this.ionPageWaitTimeout);
|
|
1670
|
-
this.ionPageWaitTimeout = undefined;
|
|
1671
|
-
}
|
|
1672
|
-
if (this.outOfScopeUnmountTimeout) {
|
|
1673
|
-
clearTimeout(this.outOfScopeUnmountTimeout);
|
|
1674
|
-
this.outOfScopeUnmountTimeout = undefined;
|
|
1675
|
-
}
|
|
1676
|
-
this.waitingForIonPage = false;
|
|
1677
|
-
// Hide all views in this outlet before clearing.
|
|
1678
|
-
// This is critical for nested outlets - when the parent component unmounts,
|
|
1679
|
-
// the nested outlet's componentDidUpdate won't be called, so we must hide
|
|
1680
|
-
// the ion-page elements here to prevent them from remaining visible on top
|
|
1681
|
-
// of other content after navigation to a different route.
|
|
1682
|
-
const allViewsInOutlet = this.context.getViewItemsForOutlet ? this.context.getViewItemsForOutlet(this.id) : [];
|
|
1683
|
-
allViewsInOutlet.forEach((viewItem) => {
|
|
1684
|
-
hideIonPageElement(viewItem.ionPageElement);
|
|
1685
|
-
});
|
|
1686
258
|
this.clearOutletTimeout = this.context.clearOutlet(this.id);
|
|
1687
259
|
}
|
|
1688
|
-
/**
|
|
1689
|
-
* Sets the transition between pages within this router outlet.
|
|
1690
|
-
* This function determines the entering and leaving views based on the
|
|
1691
|
-
* provided route information and triggers the appropriate animation.
|
|
1692
|
-
* It also handles scenarios like initial loads, back navigation, and
|
|
1693
|
-
* navigation to the same view with different parameters.
|
|
1694
|
-
*
|
|
1695
|
-
* @param routeInfo It contains info about the current route,
|
|
1696
|
-
* the previous route, and the action taken (e.g., push, replace).
|
|
1697
|
-
*
|
|
1698
|
-
* @returns A promise that resolves when the transition is complete.
|
|
1699
|
-
* If no transition is needed or if the router outlet isn't ready,
|
|
1700
|
-
* the Promise may resolve immediately.
|
|
1701
|
-
*/
|
|
1702
260
|
async handlePageTransition(routeInfo) {
|
|
1703
|
-
var _a;
|
|
1704
|
-
// Wait for router outlet to mount
|
|
261
|
+
var _a, _b;
|
|
1705
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
|
+
*/
|
|
1706
270
|
this.pendingPageTransition = true;
|
|
1707
|
-
return;
|
|
1708
271
|
}
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
// Get parent path for nested outlets
|
|
1715
|
-
const parentPath = this.getParentPath();
|
|
1716
|
-
// Handle out-of-scope outlet (route outside mount path)
|
|
1717
|
-
if (this.handleOutOfScopeOutlet(routeInfo)) {
|
|
1718
|
-
return;
|
|
1719
|
-
}
|
|
1720
|
-
// Clear any pending out-of-scope unmount timeout
|
|
1721
|
-
if (this.outOfScopeUnmountTimeout) {
|
|
1722
|
-
clearTimeout(this.outOfScopeUnmountTimeout);
|
|
1723
|
-
this.outOfScopeUnmountTimeout = undefined;
|
|
1724
|
-
}
|
|
1725
|
-
// Handle nested outlet with relative routes but no valid parent path
|
|
1726
|
-
if (this.handleOutOfContextNestedOutlet(parentPath, leavingViewItem)) {
|
|
1727
|
-
return;
|
|
1728
|
-
}
|
|
1729
|
-
// Find the matching route element
|
|
1730
|
-
const enteringRoute = findRouteByRouteInfo((_a = this.ionRouterOutlet) === null || _a === void 0 ? void 0 : _a.props.children, routeInfo, parentPath);
|
|
1731
|
-
// Handle nested outlet with no matching route
|
|
1732
|
-
if (this.handleNoMatchingRoute(enteringRoute, enteringViewItem, leavingViewItem)) {
|
|
1733
|
-
return;
|
|
1734
|
-
}
|
|
1735
|
-
// Create or update the entering view item
|
|
1736
|
-
if (enteringViewItem && enteringRoute) {
|
|
1737
|
-
enteringViewItem.reactElement = enteringRoute;
|
|
1738
|
-
}
|
|
1739
|
-
else if (enteringRoute) {
|
|
1740
|
-
enteringViewItem = this.context.createViewItem(this.id, enteringRoute, routeInfo);
|
|
1741
|
-
this.context.addViewItem(enteringViewItem);
|
|
1742
|
-
}
|
|
1743
|
-
// Handle transition based on ion-page element availability
|
|
1744
|
-
// Check if the ionPageElement is still in the document.
|
|
1745
|
-
// If the view was previously unmounted (mount=false), the ViewLifeCycleManager
|
|
1746
|
-
// removes the React component from the tree, which removes the IonPage from the DOM.
|
|
1747
|
-
// The ionPageElement reference becomes stale and we need to wait for a new one.
|
|
1748
|
-
const ionPageIsInDocument = (enteringViewItem === null || enteringViewItem === void 0 ? void 0 : enteringViewItem.ionPageElement) && document.body.contains(enteringViewItem.ionPageElement);
|
|
1749
|
-
if (enteringViewItem && ionPageIsInDocument) {
|
|
1750
|
-
// Clear waiting state
|
|
1751
|
-
if (this.waitingForIonPage) {
|
|
1752
|
-
this.waitingForIonPage = false;
|
|
1753
|
-
}
|
|
1754
|
-
if (this.ionPageWaitTimeout) {
|
|
1755
|
-
clearTimeout(this.ionPageWaitTimeout);
|
|
1756
|
-
this.ionPageWaitTimeout = undefined;
|
|
1757
|
-
}
|
|
1758
|
-
this.handleReadyEnteringView(routeInfo, enteringViewItem, leavingViewItem, shouldUnmountLeavingViewItem);
|
|
1759
|
-
}
|
|
1760
|
-
else if (enteringViewItem && !ionPageIsInDocument) {
|
|
1761
|
-
// Wait for ion-page to mount
|
|
1762
|
-
// This handles both: no ionPageElement, or stale ionPageElement (not in document)
|
|
1763
|
-
// Clear stale reference if the element is no longer in the document
|
|
1764
|
-
if (enteringViewItem.ionPageElement && !document.body.contains(enteringViewItem.ionPageElement)) {
|
|
1765
|
-
enteringViewItem.ionPageElement = undefined;
|
|
1766
|
-
}
|
|
1767
|
-
// Ensure the view is marked as mounted so ViewLifeCycleManager renders the IonPage
|
|
1768
|
-
if (!enteringViewItem.mount) {
|
|
1769
|
-
enteringViewItem.mount = true;
|
|
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);
|
|
1770
277
|
}
|
|
1771
|
-
|
|
1772
|
-
return;
|
|
1773
|
-
}
|
|
1774
|
-
else if (!enteringViewItem && !enteringRoute) {
|
|
1775
|
-
// No view or route found - likely leaving to another outlet
|
|
278
|
+
// Check if leavingViewItem should be unmounted
|
|
1776
279
|
if (leavingViewItem) {
|
|
1777
|
-
|
|
1778
|
-
if (shouldUnmountLeavingViewItem) {
|
|
280
|
+
if (routeInfo.routeAction === 'replace') {
|
|
1779
281
|
leavingViewItem.mount = false;
|
|
1780
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
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const enteringRoute = matchRoute((_b = this.ionRouterOutlet) === null || _b === void 0 ? void 0 : _b.props.children, routeInfo);
|
|
293
|
+
if (enteringViewItem) {
|
|
294
|
+
enteringViewItem.reactElement = enteringRoute;
|
|
295
|
+
}
|
|
296
|
+
else if (enteringRoute) {
|
|
297
|
+
enteringViewItem = this.context.createViewItem(this.id, enteringRoute, routeInfo);
|
|
298
|
+
this.context.addViewItem(enteringViewItem);
|
|
299
|
+
}
|
|
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;
|
|
333
|
+
}
|
|
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');
|
|
359
|
+
}
|
|
360
|
+
// }, 250);
|
|
1781
361
|
}
|
|
362
|
+
this.forceUpdate();
|
|
1782
363
|
}
|
|
1783
|
-
this.forceUpdate();
|
|
1784
364
|
}
|
|
1785
|
-
/**
|
|
1786
|
-
* Registers an `<IonPage>` DOM element with the `StackManager`.
|
|
1787
|
-
* This is called when `<IonPage>` has been mounted.
|
|
1788
|
-
*
|
|
1789
|
-
* @param page The element of the rendered `<IonPage>`.
|
|
1790
|
-
* @param routeInfo The route information that associates with `<IonPage>`.
|
|
1791
|
-
*/
|
|
1792
365
|
registerIonPage(page, routeInfo) {
|
|
1793
|
-
this.waitingForIonPage = false;
|
|
1794
|
-
if (this.ionPageWaitTimeout) {
|
|
1795
|
-
clearTimeout(this.ionPageWaitTimeout);
|
|
1796
|
-
this.ionPageWaitTimeout = undefined;
|
|
1797
|
-
}
|
|
1798
|
-
this.pendingPageTransition = false;
|
|
1799
366
|
const foundView = this.context.findViewItemByRouteInfo(routeInfo, this.id);
|
|
1800
367
|
if (foundView) {
|
|
1801
368
|
const oldPageElement = foundView.ionPageElement;
|
|
1802
|
-
/**
|
|
1803
|
-
* FIX for issue #28878: Reject orphaned IonPage registrations.
|
|
1804
|
-
*
|
|
1805
|
-
* When a component conditionally renders different IonPages (e.g., list vs empty state)
|
|
1806
|
-
* using React keys, and state changes simultaneously with navigation, the new IonPage
|
|
1807
|
-
* tries to register for a route we're navigating away from. This creates a stale view.
|
|
1808
|
-
*
|
|
1809
|
-
* Only reject if both pageIds exist and differ, to allow nested outlet registrations.
|
|
1810
|
-
*/
|
|
1811
|
-
if (this.shouldRejectOrphanedPage(page, oldPageElement, routeInfo)) {
|
|
1812
|
-
this.hideAndRemoveOrphanedPage(page);
|
|
1813
|
-
return;
|
|
1814
|
-
}
|
|
1815
369
|
foundView.ionPageElement = page;
|
|
1816
370
|
foundView.ionRoute = true;
|
|
1817
371
|
/**
|
|
@@ -1825,89 +379,48 @@ class StackManager extends React.PureComponent {
|
|
|
1825
379
|
}
|
|
1826
380
|
this.handlePageTransition(routeInfo);
|
|
1827
381
|
}
|
|
1828
|
-
/**
|
|
1829
|
-
* Determines if a new IonPage registration should be rejected as orphaned.
|
|
1830
|
-
* This happens when a component re-renders with a different IonPage while navigating away.
|
|
1831
|
-
*/
|
|
1832
|
-
shouldRejectOrphanedPage(newPage, oldPageElement, routeInfo) {
|
|
1833
|
-
if (!oldPageElement || oldPageElement === newPage) {
|
|
1834
|
-
return false;
|
|
1835
|
-
}
|
|
1836
|
-
const newPageId = newPage.getAttribute('data-pageid');
|
|
1837
|
-
const oldPageId = oldPageElement.getAttribute('data-pageid');
|
|
1838
|
-
// Only reject if both pageIds exist and are different
|
|
1839
|
-
if (!newPageId || !oldPageId || newPageId === oldPageId) {
|
|
1840
|
-
return false;
|
|
1841
|
-
}
|
|
1842
|
-
// Reject only if we're navigating away from this route
|
|
1843
|
-
return this.props.routeInfo.pathname !== routeInfo.pathname;
|
|
1844
|
-
}
|
|
1845
|
-
/**
|
|
1846
|
-
* Hides an orphaned IonPage and schedules its removal from the DOM.
|
|
1847
|
-
*/
|
|
1848
|
-
hideAndRemoveOrphanedPage(page) {
|
|
1849
|
-
page.classList.add('ion-page-hidden');
|
|
1850
|
-
page.setAttribute('aria-hidden', 'true');
|
|
1851
|
-
setTimeout(() => {
|
|
1852
|
-
if (page.parentElement) {
|
|
1853
|
-
page.remove();
|
|
1854
|
-
}
|
|
1855
|
-
}, VIEW_UNMOUNT_DELAY_MS);
|
|
1856
|
-
}
|
|
1857
|
-
/**
|
|
1858
|
-
* Configures the router outlet for the swipe-to-go-back gesture.
|
|
1859
|
-
*
|
|
1860
|
-
* @param routerOutlet The Ionic router outlet component: `<IonRouterOutlet>`.
|
|
1861
|
-
*/
|
|
1862
382
|
async setupRouterOutlet(routerOutlet) {
|
|
1863
383
|
const canStart = () => {
|
|
1864
384
|
const config = getConfig();
|
|
1865
|
-
// Check if swipe back is enabled in config (default to true for iOS mode)
|
|
1866
385
|
const swipeEnabled = config && config.get('swipeBackEnabled', routerOutlet.mode === 'ios');
|
|
1867
386
|
if (!swipeEnabled) {
|
|
1868
387
|
return false;
|
|
1869
388
|
}
|
|
1870
389
|
const { routeInfo } = this.props;
|
|
1871
|
-
const
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
enteringViewItem.routeData.match.pattern.path !== routeInfo.pathname;
|
|
1893
|
-
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);
|
|
1894
411
|
};
|
|
1895
412
|
const onStart = async () => {
|
|
1896
413
|
const { routeInfo } = this.props;
|
|
1897
|
-
const
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, undefined, false);
|
|
1902
|
-
}
|
|
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);
|
|
1903
418
|
const leavingViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id, false);
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
}
|
|
1910
|
-
// 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
|
+
*/
|
|
1911
424
|
if (enteringViewItem && leavingViewItem) {
|
|
1912
425
|
await this.transitionPage(routeInfo, enteringViewItem, leavingViewItem, 'back', true);
|
|
1913
426
|
}
|
|
@@ -1915,23 +428,34 @@ class StackManager extends React.PureComponent {
|
|
|
1915
428
|
};
|
|
1916
429
|
const onEnd = (shouldContinue) => {
|
|
1917
430
|
if (shouldContinue) {
|
|
1918
|
-
// User finished the swipe gesture, so complete the back navigation
|
|
1919
431
|
this.skipTransition = true;
|
|
1920
432
|
this.context.goBack();
|
|
1921
433
|
}
|
|
1922
434
|
else {
|
|
1923
|
-
|
|
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
|
+
*/
|
|
1924
440
|
const { routeInfo } = this.props;
|
|
1925
|
-
const
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, undefined, false);
|
|
1930
|
-
}
|
|
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);
|
|
1931
445
|
const leavingViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id, false);
|
|
1932
|
-
|
|
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
|
+
*/
|
|
1933
455
|
if (enteringViewItem !== leavingViewItem && (enteringViewItem === null || enteringViewItem === void 0 ? void 0 : enteringViewItem.ionPageElement) !== undefined) {
|
|
1934
|
-
|
|
456
|
+
const { ionPageElement } = enteringViewItem;
|
|
457
|
+
ionPageElement.setAttribute('aria-hidden', 'true');
|
|
458
|
+
ionPageElement.classList.add('ion-page-hidden');
|
|
1935
459
|
}
|
|
1936
460
|
}
|
|
1937
461
|
};
|
|
@@ -1941,18 +465,6 @@ class StackManager extends React.PureComponent {
|
|
|
1941
465
|
onEnd,
|
|
1942
466
|
};
|
|
1943
467
|
}
|
|
1944
|
-
/**
|
|
1945
|
-
* Animates the transition between the entering and leaving pages within the
|
|
1946
|
-
* router outlet.
|
|
1947
|
-
*
|
|
1948
|
-
* @param routeInfo Info about the current route.
|
|
1949
|
-
* @param enteringViewItem The view item that is entering.
|
|
1950
|
-
* @param leavingViewItem The view item that is leaving.
|
|
1951
|
-
* @param direction The direction of the transition.
|
|
1952
|
-
* @param progressAnimation Indicates if the transition is part of a
|
|
1953
|
-
* gesture controlled animation (e.g., swipe to go back).
|
|
1954
|
-
* Defaults to `false`.
|
|
1955
|
-
*/
|
|
1956
468
|
async transitionPage(routeInfo, enteringViewItem, leavingViewItem, direction, progressAnimation = false) {
|
|
1957
469
|
const runCommit = async (enteringEl, leavingEl) => {
|
|
1958
470
|
const skipTransition = this.skipTransition;
|
|
@@ -1998,8 +510,7 @@ class StackManager extends React.PureComponent {
|
|
|
1998
510
|
if (leavingViewItem && leavingViewItem.ionPageElement && enteringViewItem === leavingViewItem) {
|
|
1999
511
|
// If a page is transitioning to another version of itself
|
|
2000
512
|
// we clone it so we can have an animation to show
|
|
2001
|
-
|
|
2002
|
-
const match = matchComponent(leavingViewItem.reactElement, routeInfo.pathname);
|
|
513
|
+
const match = matchComponent(leavingViewItem.reactElement, routeInfo.pathname, true);
|
|
2003
514
|
if (match) {
|
|
2004
515
|
const newLeavingElement = clonePageElement(leavingViewItem.ionPageElement.outerHTML);
|
|
2005
516
|
if (newLeavingElement) {
|
|
@@ -2009,15 +520,6 @@ class StackManager extends React.PureComponent {
|
|
|
2009
520
|
}
|
|
2010
521
|
}
|
|
2011
522
|
else {
|
|
2012
|
-
/**
|
|
2013
|
-
* The route no longer matches the component type of the leaving view.
|
|
2014
|
-
* (e.g., `/user/1` → `/settings`)
|
|
2015
|
-
*
|
|
2016
|
-
* This can also occur in edge cases like rapid navigation
|
|
2017
|
-
* or during parent component re-renders that briefly cause
|
|
2018
|
-
* the view items to be the same instance before the final
|
|
2019
|
-
* route component is determined.
|
|
2020
|
-
*/
|
|
2021
523
|
await runCommit(enteringViewItem.ionPageElement, undefined);
|
|
2022
524
|
}
|
|
2023
525
|
}
|
|
@@ -2033,25 +535,20 @@ class StackManager extends React.PureComponent {
|
|
|
2033
535
|
render() {
|
|
2034
536
|
const { children } = this.props;
|
|
2035
537
|
const ionRouterOutlet = React.Children.only(children);
|
|
2036
|
-
// Store reference for use in getParentPath() and handlePageTransition()
|
|
2037
538
|
this.ionRouterOutlet = ionRouterOutlet;
|
|
2038
539
|
const components = this.context.getChildrenToRender(this.id, this.ionRouterOutlet, this.props.routeInfo, () => {
|
|
2039
|
-
// Callback triggers re-render when view items are modified during getChildrenToRender
|
|
2040
540
|
this.forceUpdate();
|
|
2041
541
|
});
|
|
2042
542
|
return (React.createElement(StackContext.Provider, { value: this.stackContextValue }, React.cloneElement(ionRouterOutlet, {
|
|
2043
543
|
ref: (node) => {
|
|
2044
544
|
if (ionRouterOutlet.props.setRef) {
|
|
2045
|
-
// Needed to handle external refs from devs.
|
|
2046
545
|
ionRouterOutlet.props.setRef(node);
|
|
2047
546
|
}
|
|
2048
547
|
if (ionRouterOutlet.props.forwardedRef) {
|
|
2049
|
-
// Needed to handle external refs from devs.
|
|
2050
548
|
ionRouterOutlet.props.forwardedRef.current = node;
|
|
2051
549
|
}
|
|
2052
550
|
this.routerOutletElement = node;
|
|
2053
551
|
const { ref } = ionRouterOutlet;
|
|
2054
|
-
// Check for legacy refs.
|
|
2055
552
|
if (typeof ref === 'function') {
|
|
2056
553
|
ref(node);
|
|
2057
554
|
}
|
|
@@ -2062,389 +559,169 @@ class StackManager extends React.PureComponent {
|
|
|
2062
559
|
return RouteManagerContext;
|
|
2063
560
|
}
|
|
2064
561
|
}
|
|
2065
|
-
|
|
2066
|
-
* Finds the `<Route />` node matching the current route info.
|
|
2067
|
-
* If no `<Route />` can be matched, a fallback node is returned.
|
|
2068
|
-
* Routes are prioritized by specificity (most specific first).
|
|
2069
|
-
*
|
|
2070
|
-
* @param node The root node to search for `<Route />` nodes.
|
|
2071
|
-
* @param routeInfo The route information to match against.
|
|
2072
|
-
* @param parentPath The parent path that was matched by the parent outlet (for nested routing)
|
|
2073
|
-
*/
|
|
2074
|
-
function findRouteByRouteInfo(node, routeInfo, parentPath) {
|
|
2075
|
-
var _a;
|
|
562
|
+
function matchRoute(node, routeInfo) {
|
|
2076
563
|
let matchedNode;
|
|
2077
|
-
|
|
2078
|
-
// `<Route />` nodes are rendered inside of a <Routes /> node
|
|
2079
|
-
const routesChildren = (_a = getRoutesChildren(node)) !== null && _a !== void 0 ? _a : node;
|
|
2080
|
-
// Collect all route children
|
|
2081
|
-
const routeChildren = React.Children.toArray(routesChildren).filter((child) => React.isValidElement(child) && child.type === Route);
|
|
2082
|
-
// Sort routes by specificity (most specific first)
|
|
2083
|
-
const sortedRoutes = routeChildren.sort((a, b) => {
|
|
2084
|
-
const pathA = a.props.path || '';
|
|
2085
|
-
const pathB = b.props.path || '';
|
|
2086
|
-
// Index routes come first
|
|
2087
|
-
if (a.props.index && !b.props.index)
|
|
2088
|
-
return -1;
|
|
2089
|
-
if (!a.props.index && b.props.index)
|
|
2090
|
-
return 1;
|
|
2091
|
-
// Wildcard-only routes (*) should come LAST
|
|
2092
|
-
const aIsWildcardOnly = pathA === '*';
|
|
2093
|
-
const bIsWildcardOnly = pathB === '*';
|
|
2094
|
-
if (!aIsWildcardOnly && bIsWildcardOnly)
|
|
2095
|
-
return -1;
|
|
2096
|
-
if (aIsWildcardOnly && !bIsWildcardOnly)
|
|
2097
|
-
return 1;
|
|
2098
|
-
// Exact matches (no wildcards/params) come before wildcard/param routes
|
|
2099
|
-
const aHasWildcard = pathA.includes('*') || pathA.includes(':');
|
|
2100
|
-
const bHasWildcard = pathB.includes('*') || pathB.includes(':');
|
|
2101
|
-
if (!aHasWildcard && bHasWildcard)
|
|
2102
|
-
return -1;
|
|
2103
|
-
if (aHasWildcard && !bHasWildcard)
|
|
2104
|
-
return 1;
|
|
2105
|
-
// Among routes with same wildcard status, longer paths are more specific
|
|
2106
|
-
if (pathA.length !== pathB.length) {
|
|
2107
|
-
return pathB.length - pathA.length;
|
|
2108
|
-
}
|
|
2109
|
-
return 0;
|
|
2110
|
-
});
|
|
2111
|
-
// For nested routes in React Router 6, we need to extract the relative path
|
|
2112
|
-
// that this outlet should be responsible for matching
|
|
2113
|
-
const originalPathname = routeInfo.pathname;
|
|
2114
|
-
let relativePathnameToMatch = routeInfo.pathname;
|
|
2115
|
-
// Check if we have relative routes (routes that don't start with '/')
|
|
2116
|
-
const hasRelativeRoutes = sortedRoutes.some((r) => r.props.path && !r.props.path.startsWith('/'));
|
|
2117
|
-
const hasIndexRoute = sortedRoutes.some((r) => r.props.index);
|
|
2118
|
-
// SIMPLIFIED: Trust React Router 6's matching more, compute relative path when parent is known
|
|
2119
|
-
if ((hasRelativeRoutes || hasIndexRoute) && parentPath) {
|
|
2120
|
-
const parentPrefix = parentPath.replace('/*', '');
|
|
2121
|
-
// Normalize both paths to start with '/' for consistent comparison
|
|
2122
|
-
const normalizedParent = stripTrailingSlash(parentPrefix.startsWith('/') ? parentPrefix : `/${parentPrefix}`);
|
|
2123
|
-
const normalizedPathname = stripTrailingSlash(routeInfo.pathname);
|
|
2124
|
-
// Only compute relative path if pathname is within parent scope
|
|
2125
|
-
if (normalizedPathname.startsWith(normalizedParent + '/') || normalizedPathname === normalizedParent) {
|
|
2126
|
-
const pathSegments = routeInfo.pathname.split('/').filter(Boolean);
|
|
2127
|
-
const parentSegments = normalizedParent.split('/').filter(Boolean);
|
|
2128
|
-
const relativeSegments = pathSegments.slice(parentSegments.length);
|
|
2129
|
-
relativePathnameToMatch = relativeSegments.join('/'); // Empty string is valid for index routes
|
|
2130
|
-
}
|
|
2131
|
-
}
|
|
2132
|
-
// Find the first matching route
|
|
2133
|
-
for (const child of sortedRoutes) {
|
|
2134
|
-
const childPath = child.props.path;
|
|
2135
|
-
const isAbsoluteRoute = childPath && childPath.startsWith('/');
|
|
2136
|
-
// Determine which pathname to match against:
|
|
2137
|
-
// - For absolute routes: use the original full pathname
|
|
2138
|
-
// - For relative routes with a parent: use the computed relative pathname
|
|
2139
|
-
// - For relative routes at root level (no parent): use the original pathname
|
|
2140
|
-
// (matchPath will handle the relative-to-absolute normalization)
|
|
2141
|
-
const pathnameToMatch = isAbsoluteRoute ? originalPathname : relativePathnameToMatch;
|
|
2142
|
-
// Determine the path portion to match:
|
|
2143
|
-
// - For absolute routes: use derivePathnameToMatch
|
|
2144
|
-
// - For relative routes at root level (no parent): use original pathname
|
|
2145
|
-
// directly since matchPath normalizes both path and pathname
|
|
2146
|
-
// - For relative routes with parent: use derivePathnameToMatch for wildcards,
|
|
2147
|
-
// or the computed relative pathname for non-wildcards
|
|
2148
|
-
let pathForMatch;
|
|
2149
|
-
if (isAbsoluteRoute) {
|
|
2150
|
-
pathForMatch = derivePathnameToMatch(pathnameToMatch, childPath);
|
|
2151
|
-
}
|
|
2152
|
-
else if (!parentPath && childPath) {
|
|
2153
|
-
// Root-level relative route: use the full pathname and let matchPath
|
|
2154
|
-
// handle the normalization (it adds '/' to both path and pathname)
|
|
2155
|
-
pathForMatch = originalPathname;
|
|
2156
|
-
}
|
|
2157
|
-
else if (childPath && childPath.includes('*')) {
|
|
2158
|
-
// Relative wildcard route with parent path: use derivePathnameToMatch
|
|
2159
|
-
pathForMatch = derivePathnameToMatch(pathnameToMatch, childPath);
|
|
2160
|
-
}
|
|
2161
|
-
else {
|
|
2162
|
-
pathForMatch = pathnameToMatch;
|
|
2163
|
-
}
|
|
564
|
+
React.Children.forEach(node, (child) => {
|
|
2164
565
|
const match = matchPath({
|
|
2165
|
-
pathname:
|
|
566
|
+
pathname: routeInfo.pathname,
|
|
2166
567
|
componentProps: child.props,
|
|
2167
568
|
});
|
|
2168
569
|
if (match) {
|
|
2169
570
|
matchedNode = child;
|
|
2170
|
-
break;
|
|
2171
571
|
}
|
|
2172
|
-
}
|
|
572
|
+
});
|
|
2173
573
|
if (matchedNode) {
|
|
2174
574
|
return matchedNode;
|
|
2175
575
|
}
|
|
2176
|
-
// If we haven't found a node
|
|
2177
|
-
//
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
let isPathnameInScope = true;
|
|
2182
|
-
if (absolutePathRoutes.length > 0) {
|
|
2183
|
-
// Find common prefix of all absolute paths to determine outlet scope
|
|
2184
|
-
const absolutePaths = absolutePathRoutes.map((r) => r.props.path);
|
|
2185
|
-
const commonPrefix = computeCommonPrefix(absolutePaths);
|
|
2186
|
-
// If we have a common prefix, check if the current pathname is within that scope
|
|
2187
|
-
if (commonPrefix && commonPrefix !== '/') {
|
|
2188
|
-
isPathnameInScope = routeInfo.pathname.startsWith(commonPrefix);
|
|
2189
|
-
}
|
|
2190
|
-
}
|
|
2191
|
-
// Only look for fallback route if pathname is within scope
|
|
2192
|
-
if (isPathnameInScope) {
|
|
2193
|
-
for (const child of routeChildren) {
|
|
2194
|
-
if (!child.props.path) {
|
|
2195
|
-
fallbackNode = child;
|
|
2196
|
-
break;
|
|
2197
|
-
}
|
|
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;
|
|
2198
581
|
}
|
|
2199
|
-
}
|
|
2200
|
-
return matchedNode
|
|
582
|
+
});
|
|
583
|
+
return matchedNode;
|
|
2201
584
|
}
|
|
2202
585
|
function matchComponent(node, pathname, forceExact) {
|
|
2203
|
-
var _a;
|
|
2204
|
-
const routePath = (_a = node === null || node === void 0 ? void 0 : node.props) === null || _a === void 0 ? void 0 : _a.path;
|
|
2205
|
-
const pathnameToMatch = derivePathnameToMatch(pathname, routePath);
|
|
2206
586
|
return matchPath({
|
|
2207
|
-
pathname
|
|
2208
|
-
componentProps: Object.assign(Object.assign({}, node.props), {
|
|
587
|
+
pathname,
|
|
588
|
+
componentProps: Object.assign(Object.assign({}, node.props), { exact: forceExact }),
|
|
2209
589
|
});
|
|
2210
590
|
}
|
|
2211
591
|
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
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
|
+
};
|
|
2236
626
|
}
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
const valueB = paramsB[key];
|
|
2240
|
-
if (Array.isArray(valueA) && Array.isArray(valueB)) {
|
|
2241
|
-
if (valueA.length !== valueB.length) {
|
|
2242
|
-
return false;
|
|
2243
|
-
}
|
|
2244
|
-
return valueA.every((entry, idx) => entry === valueB[idx]);
|
|
2245
|
-
}
|
|
2246
|
-
return valueA === valueB;
|
|
2247
|
-
});
|
|
2248
|
-
};
|
|
2249
|
-
const IonRouter = ({ children, registerHistoryListener }) => {
|
|
2250
|
-
const location = useLocation();
|
|
2251
|
-
const navigate = useNavigate();
|
|
2252
|
-
const didMountRef = useRef(false);
|
|
2253
|
-
const locationHistory = useRef(new LocationHistory());
|
|
2254
|
-
const currentTab = useRef(undefined);
|
|
2255
|
-
const viewStack = useRef(new ReactRouterViewStack());
|
|
2256
|
-
const incomingRouteParams = useRef(null);
|
|
2257
|
-
const [routeInfo, setRouteInfo] = useState({
|
|
2258
|
-
id: generateId('routeInfo'),
|
|
2259
|
-
pathname: location.pathname,
|
|
2260
|
-
search: location.search,
|
|
2261
|
-
params: {},
|
|
2262
|
-
});
|
|
2263
|
-
useEffect(() => {
|
|
2264
|
-
if (didMountRef.current) {
|
|
627
|
+
handleChangeTab(tab, path, routeOptions) {
|
|
628
|
+
if (!path) {
|
|
2265
629
|
return;
|
|
2266
630
|
}
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
if (areParamsEqual(routeInfo.params, paramsCopy)) {
|
|
2281
|
-
return;
|
|
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 : ''));
|
|
2282
644
|
}
|
|
2283
|
-
const updatedRouteInfo = Object.assign(Object.assign({}, routeInfo), { params: paramsCopy });
|
|
2284
|
-
locationHistory.current.update(updatedRouteInfo);
|
|
2285
|
-
setRouteInfo(updatedRouteInfo);
|
|
2286
645
|
}
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
*
|
|
2294
|
-
* @param location The current location object from the history.
|
|
2295
|
-
* @param action The action that triggered the history change.
|
|
2296
|
-
*/
|
|
2297
|
-
const handleHistoryChange = (location, action) => {
|
|
2298
|
-
var _a, _b, _c, _d, _e;
|
|
646
|
+
else {
|
|
647
|
+
this.handleNavigate(pathname, 'push', 'none', undefined, routeOptions, tab);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
handleHistoryChange(location, action) {
|
|
651
|
+
var _a, _b, _c;
|
|
2299
652
|
let leavingLocationInfo;
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
*/
|
|
2304
|
-
if (incomingRouteParams.current) {
|
|
2305
|
-
/**
|
|
2306
|
-
* The current history entry is overwritten, so the previous entry
|
|
2307
|
-
* is the one we are leaving.
|
|
2308
|
-
*/
|
|
2309
|
-
if (((_a = incomingRouteParams.current) === null || _a === void 0 ? void 0 : _a.routeAction) === 'replace') {
|
|
2310
|
-
leavingLocationInfo = locationHistory.current.previous();
|
|
653
|
+
if (this.incomingRouteParams) {
|
|
654
|
+
if (this.incomingRouteParams.routeAction === 'replace') {
|
|
655
|
+
leavingLocationInfo = this.locationHistory.previous();
|
|
2311
656
|
}
|
|
2312
657
|
else {
|
|
2313
|
-
|
|
2314
|
-
leavingLocationInfo = locationHistory.current.current();
|
|
658
|
+
leavingLocationInfo = this.locationHistory.current();
|
|
2315
659
|
}
|
|
2316
660
|
}
|
|
2317
661
|
else {
|
|
2318
|
-
|
|
2319
|
-
* An external navigation was triggered
|
|
2320
|
-
* e.g., browser back/forward button or direct link
|
|
2321
|
-
*
|
|
2322
|
-
* The leaving location is the current route.
|
|
2323
|
-
*/
|
|
2324
|
-
leavingLocationInfo = locationHistory.current.current();
|
|
662
|
+
leavingLocationInfo = this.locationHistory.current();
|
|
2325
663
|
}
|
|
2326
664
|
const leavingUrl = leavingLocationInfo.pathname + leavingLocationInfo.search;
|
|
2327
665
|
if (leavingUrl !== location.pathname) {
|
|
2328
|
-
if (!incomingRouteParams
|
|
2329
|
-
// Determine if the destination is a tab route by checking if it matches
|
|
2330
|
-
// the pattern of tab routes (containing /tabs/ in the path)
|
|
2331
|
-
const isTabRoute = /\/tabs(\/|$)/.test(location.pathname);
|
|
2332
|
-
const tabToUse = isTabRoute ? currentTab.current : undefined;
|
|
2333
|
-
// If we're leaving tabs entirely, clear the current tab
|
|
2334
|
-
if (!isTabRoute && currentTab.current) {
|
|
2335
|
-
currentTab.current = undefined;
|
|
2336
|
-
}
|
|
2337
|
-
/**
|
|
2338
|
-
* A `REPLACE` action can be triggered by React Router's
|
|
2339
|
-
* `<Redirect />` component.
|
|
2340
|
-
*/
|
|
666
|
+
if (!this.incomingRouteParams) {
|
|
2341
667
|
if (action === 'REPLACE') {
|
|
2342
|
-
incomingRouteParams
|
|
668
|
+
this.incomingRouteParams = {
|
|
2343
669
|
routeAction: 'replace',
|
|
2344
670
|
routeDirection: 'none',
|
|
2345
|
-
tab:
|
|
671
|
+
tab: this.currentTab,
|
|
2346
672
|
};
|
|
2347
673
|
}
|
|
2348
|
-
/**
|
|
2349
|
-
* A `POP` action can be triggered by the browser's back/forward
|
|
2350
|
-
* button.
|
|
2351
|
-
*/
|
|
2352
674
|
if (action === 'POP') {
|
|
2353
|
-
const currentRoute = locationHistory.current
|
|
2354
|
-
/**
|
|
2355
|
-
* Check if the current route was "pushed" by a previous route
|
|
2356
|
-
* (indicates a linear history path).
|
|
2357
|
-
*/
|
|
675
|
+
const currentRoute = this.locationHistory.current();
|
|
2358
676
|
if (currentRoute && currentRoute.pushedByRoute) {
|
|
2359
|
-
const prevInfo = locationHistory.
|
|
2360
|
-
incomingRouteParams
|
|
2361
|
-
// It's a non-linear history path like a direct link.
|
|
677
|
+
const prevInfo = this.locationHistory.findLastLocation(currentRoute);
|
|
678
|
+
this.incomingRouteParams = Object.assign(Object.assign({}, prevInfo), { routeAction: 'pop', routeDirection: 'back' });
|
|
2362
679
|
}
|
|
2363
680
|
else {
|
|
2364
|
-
incomingRouteParams
|
|
681
|
+
this.incomingRouteParams = {
|
|
2365
682
|
routeAction: 'pop',
|
|
2366
683
|
routeDirection: 'none',
|
|
2367
|
-
tab:
|
|
684
|
+
tab: this.currentTab,
|
|
2368
685
|
};
|
|
2369
686
|
}
|
|
2370
687
|
}
|
|
2371
|
-
if (!incomingRouteParams
|
|
2372
|
-
|
|
2373
|
-
incomingRouteParams.current = {
|
|
688
|
+
if (!this.incomingRouteParams) {
|
|
689
|
+
this.incomingRouteParams = {
|
|
2374
690
|
routeAction: 'push',
|
|
2375
|
-
routeDirection: (state === null ||
|
|
2376
|
-
routeOptions: state === null ||
|
|
2377
|
-
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,
|
|
2378
694
|
};
|
|
2379
695
|
}
|
|
2380
696
|
}
|
|
2381
697
|
let routeInfo;
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
}
|
|
2386
|
-
/**
|
|
2387
|
-
* An existing id indicates that it's re-activating an existing route.
|
|
2388
|
-
* e.g., tab switching or navigating back to a previous route
|
|
2389
|
-
*/
|
|
2390
|
-
if ((_b = incomingRouteParams.current) === null || _b === void 0 ? void 0 : _b.id) {
|
|
2391
|
-
routeInfo = Object.assign(Object.assign({}, incomingRouteParams.current), { lastPathname: leavingLocationInfo.pathname });
|
|
2392
|
-
locationHistory.current.add(routeInfo);
|
|
2393
|
-
/**
|
|
2394
|
-
* A new route is being created since it's not re-activating
|
|
2395
|
-
* an existing route.
|
|
2396
|
-
*/
|
|
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);
|
|
2397
701
|
}
|
|
2398
702
|
else {
|
|
2399
|
-
const isPushed =
|
|
2400
|
-
|
|
2401
|
-
routeInfo = Object.assign(Object.assign({ id: generateId('routeInfo') }, incomingRouteParams.current), { lastPathname: leavingLocationInfo.pathname, pathname: location.pathname, search: location.search, params: ((_d = incomingRouteParams.current) === null || _d === void 0 ? void 0 : _d.params)
|
|
2402
|
-
? filterUndefinedParams(incomingRouteParams.current.params)
|
|
2403
|
-
: {}, 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 });
|
|
2404
705
|
if (isPushed) {
|
|
2405
|
-
|
|
2406
|
-
// This preserves tab context for same-tab navigation while allowing cross-tab navigation.
|
|
2407
|
-
routeInfo.tab = routeInfo.tab || leavingLocationInfo.tab;
|
|
706
|
+
routeInfo.tab = leavingLocationInfo.tab;
|
|
2408
707
|
routeInfo.pushedByRoute = leavingLocationInfo.pathname;
|
|
2409
|
-
// Triggered by a browser back button or handleNavigateBack.
|
|
2410
708
|
}
|
|
2411
709
|
else if (routeInfo.routeAction === 'pop') {
|
|
2412
|
-
|
|
2413
|
-
const r = locationHistory.current.findLastLocation(routeInfo);
|
|
710
|
+
const r = this.locationHistory.findLastLocation(routeInfo);
|
|
2414
711
|
routeInfo.pushedByRoute = r === null || r === void 0 ? void 0 : r.pushedByRoute;
|
|
2415
|
-
// Navigating to a new tab.
|
|
2416
712
|
}
|
|
2417
713
|
else if (routeInfo.routeAction === 'push' && routeInfo.tab !== leavingLocationInfo.tab) {
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
*/
|
|
2422
|
-
const lastRoute = locationHistory.current.getCurrentRouteInfoForTab(routeInfo.tab);
|
|
2423
|
-
/**
|
|
2424
|
-
* Tab bar switches (direction 'none') should not create cross-tab back
|
|
2425
|
-
* navigation. Only inherit pushedByRoute from the tab's own history.
|
|
2426
|
-
*/
|
|
2427
|
-
if (routeInfo.routeDirection === 'none') {
|
|
2428
|
-
routeInfo.pushedByRoute = lastRoute === null || lastRoute === void 0 ? void 0 : lastRoute.pushedByRoute;
|
|
2429
|
-
}
|
|
2430
|
-
else {
|
|
2431
|
-
routeInfo.pushedByRoute = (_e = lastRoute === null || lastRoute === void 0 ? void 0 : lastRoute.pushedByRoute) !== null && _e !== void 0 ? _e : leavingLocationInfo.pathname;
|
|
2432
|
-
}
|
|
2433
|
-
// Triggered by `history.replace()` or a `<Redirect />` 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;
|
|
2434
717
|
}
|
|
2435
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();
|
|
2436
721
|
/**
|
|
2437
|
-
*
|
|
2438
|
-
*
|
|
2439
|
-
|
|
2440
|
-
const currentRouteInfo = locationHistory.current.current();
|
|
2441
|
-
/**
|
|
2442
|
-
* Special handling for `replace` to ensure correct `pushedByRoute`
|
|
2443
|
-
* and `lastPathname`.
|
|
2444
|
-
*
|
|
2445
|
-
* If going from `/home` to `/child`, then replacing from
|
|
2446
|
-
* `/child` to `/home`, we don't want the route info to
|
|
2447
|
-
* 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.
|
|
2448
725
|
*/
|
|
2449
726
|
const currentPushedBy = currentRouteInfo === null || currentRouteInfo === void 0 ? void 0 : currentRouteInfo.pushedByRoute;
|
|
2450
727
|
const pushedByRoute = currentPushedBy !== undefined && currentPushedBy !== routeInfo.pathname
|
|
@@ -2462,107 +739,46 @@ const IonRouter = ({ children, registerHistoryListener }) => {
|
|
|
2462
739
|
routeInfo.routeDirection = routeInfo.routeDirection || (currentRouteInfo === null || currentRouteInfo === void 0 ? void 0 : currentRouteInfo.routeDirection);
|
|
2463
740
|
routeInfo.routeAnimation = routeInfo.routeAnimation || (currentRouteInfo === null || currentRouteInfo === void 0 ? void 0 : currentRouteInfo.routeAnimation);
|
|
2464
741
|
}
|
|
2465
|
-
locationHistory.
|
|
742
|
+
this.locationHistory.add(routeInfo);
|
|
2466
743
|
}
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
};
|
|
2471
|
-
/**
|
|
2472
|
-
* Resets the specified tab to its initial, root route.
|
|
2473
|
-
*
|
|
2474
|
-
* @param tab The tab to reset.
|
|
2475
|
-
* @param originalHref The original href for the tab.
|
|
2476
|
-
* @param originalRouteOptions The original route options for the tab.
|
|
2477
|
-
*/
|
|
2478
|
-
const handleResetTab = (tab, originalHref, originalRouteOptions) => {
|
|
2479
|
-
const routeInfo = locationHistory.current.getFirstRouteInfoForTab(tab);
|
|
2480
|
-
if (routeInfo) {
|
|
2481
|
-
const newRouteInfo = Object.assign({}, routeInfo);
|
|
2482
|
-
newRouteInfo.pathname = originalHref;
|
|
2483
|
-
newRouteInfo.routeOptions = originalRouteOptions;
|
|
2484
|
-
incomingRouteParams.current = Object.assign(Object.assign({}, newRouteInfo), { routeAction: 'pop', routeDirection: 'back' });
|
|
2485
|
-
navigate(newRouteInfo.pathname + (newRouteInfo.search || ''));
|
|
744
|
+
this.setState({
|
|
745
|
+
routeInfo,
|
|
746
|
+
});
|
|
2486
747
|
}
|
|
2487
|
-
|
|
748
|
+
this.incomingRouteParams = undefined;
|
|
749
|
+
}
|
|
2488
750
|
/**
|
|
2489
|
-
*
|
|
2490
|
-
*
|
|
2491
|
-
*
|
|
2492
|
-
*
|
|
2493
|
-
* @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.
|
|
2494
755
|
*/
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
navigate(routeInfo.pathname + (routeInfo.search || ''));
|
|
2511
|
-
/**
|
|
2512
|
-
* User is navigating to a different tab.
|
|
2513
|
-
* e.g., `/tabs/home` → `/tabs/settings`
|
|
2514
|
-
*/
|
|
2515
|
-
}
|
|
2516
|
-
else {
|
|
2517
|
-
incomingRouteParams.current = Object.assign(Object.assign({}, routeParams), { pathname, search: search ? '?' + search : undefined, routeOptions });
|
|
2518
|
-
navigate(pathname + (search ? '?' + search : ''));
|
|
2519
|
-
}
|
|
2520
|
-
// 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);
|
|
2521
771
|
}
|
|
2522
772
|
else {
|
|
2523
|
-
|
|
2524
|
-
}
|
|
2525
|
-
};
|
|
2526
|
-
/**
|
|
2527
|
-
* Set the current active tab in `locationHistory`.
|
|
2528
|
-
* This is crucial for maintaining tab history since each tab has
|
|
2529
|
-
* its own navigation stack.
|
|
2530
|
-
*
|
|
2531
|
-
* @param tab The tab to set as active.
|
|
2532
|
-
*/
|
|
2533
|
-
const handleSetCurrentTab = (tab) => {
|
|
2534
|
-
currentTab.current = tab;
|
|
2535
|
-
const ri = Object.assign({}, locationHistory.current.current());
|
|
2536
|
-
if (ri.tab !== tab) {
|
|
2537
|
-
ri.tab = tab;
|
|
2538
|
-
locationHistory.current.update(ri);
|
|
773
|
+
this.props.history.replace(path);
|
|
2539
774
|
}
|
|
2540
|
-
}
|
|
2541
|
-
|
|
2542
|
-
* Handles the native back button press.
|
|
2543
|
-
* It's usually called when a user presses the platform-native back action.
|
|
2544
|
-
*/
|
|
2545
|
-
const handleNativeBack = () => {
|
|
2546
|
-
navigate(-1);
|
|
2547
|
-
};
|
|
2548
|
-
/**
|
|
2549
|
-
* Used to manage the back navigation within the Ionic React's routing
|
|
2550
|
-
* system. It's deeply integrated with Ionic's view lifecycle, animations,
|
|
2551
|
-
* and its custom history tracking (`locationHistory`) to provide a
|
|
2552
|
-
* native-like transition and maintain correct application state.
|
|
2553
|
-
*
|
|
2554
|
-
* @param defaultHref The fallback URL to navigate to if there's no
|
|
2555
|
-
* previous entry in the `locationHistory` stack.
|
|
2556
|
-
* @param routeAnimation A custom animation builder to override the
|
|
2557
|
-
* default "back" animation.
|
|
2558
|
-
*/
|
|
2559
|
-
const handleNavigateBack = (defaultHref = '/', routeAnimation) => {
|
|
775
|
+
}
|
|
776
|
+
handleNavigateBack(defaultHref = '/', routeAnimation) {
|
|
2560
777
|
const config = getConfig();
|
|
2561
778
|
defaultHref = defaultHref ? defaultHref : config && config.get('backButtonDefaultHref');
|
|
2562
|
-
const routeInfo = locationHistory.current
|
|
2563
|
-
// It's a linear navigation.
|
|
779
|
+
const routeInfo = this.locationHistory.current();
|
|
2564
780
|
if (routeInfo && routeInfo.pushedByRoute) {
|
|
2565
|
-
const prevInfo = locationHistory.
|
|
781
|
+
const prevInfo = this.locationHistory.findLastLocation(routeInfo);
|
|
2566
782
|
if (prevInfo) {
|
|
2567
783
|
/**
|
|
2568
784
|
* This needs to be passed to handleNavigate
|
|
@@ -2570,235 +786,160 @@ const IonRouter = ({ children, registerHistoryListener }) => {
|
|
|
2570
786
|
* will be overridden.
|
|
2571
787
|
*/
|
|
2572
788
|
const incomingAnimation = routeAnimation || routeInfo.routeAnimation;
|
|
2573
|
-
incomingRouteParams
|
|
2574
|
-
|
|
2575
|
-
* Check if it's a simple linear back navigation (not tabbed).
|
|
2576
|
-
* e.g., `/home` → `/settings` → back to `/home`
|
|
2577
|
-
*/
|
|
2578
|
-
const condition1 = routeInfo.lastPathname === routeInfo.pushedByRoute;
|
|
2579
|
-
const condition2 = prevInfo.pathname === routeInfo.pushedByRoute && routeInfo.tab === '' && prevInfo.tab === '';
|
|
2580
|
-
if (condition1 || condition2) {
|
|
2581
|
-
navigate(-1);
|
|
2582
|
-
}
|
|
2583
|
-
else {
|
|
789
|
+
this.incomingRouteParams = Object.assign(Object.assign({}, prevInfo), { routeAction: 'pop', routeDirection: 'back', routeAnimation: incomingAnimation });
|
|
790
|
+
if (routeInfo.lastPathname === routeInfo.pushedByRoute ||
|
|
2584
791
|
/**
|
|
2585
|
-
*
|
|
2586
|
-
*
|
|
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.
|
|
2587
796
|
*/
|
|
2588
|
-
|
|
797
|
+
(prevInfo.pathname === routeInfo.pushedByRoute && routeInfo.tab === '' && prevInfo.tab === '')) {
|
|
798
|
+
/**
|
|
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.
|
|
803
|
+
*/
|
|
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);
|
|
2589
810
|
}
|
|
2590
|
-
/**
|
|
2591
|
-
* `pushedByRoute` exists, but no corresponding previous entry in
|
|
2592
|
-
* the history stack.
|
|
2593
|
-
*/
|
|
2594
811
|
}
|
|
2595
812
|
else {
|
|
2596
|
-
handleNavigate(defaultHref, 'pop', 'back', routeAnimation);
|
|
813
|
+
this.handleNavigate(defaultHref, 'pop', 'back', routeAnimation);
|
|
2597
814
|
}
|
|
2598
|
-
/**
|
|
2599
|
-
* No `pushedByRoute` (e.g., initial page load or tab root).
|
|
2600
|
-
* Tabs with no back history should not navigate.
|
|
2601
|
-
*/
|
|
2602
815
|
}
|
|
2603
816
|
else {
|
|
2604
|
-
|
|
2605
|
-
return;
|
|
2606
|
-
}
|
|
2607
|
-
handleNavigate(defaultHref, 'pop', 'back', routeAnimation);
|
|
817
|
+
this.handleNavigate(defaultHref, 'pop', 'back', routeAnimation);
|
|
2608
818
|
}
|
|
2609
|
-
}
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
* @param routeOptions Additional options for the route.
|
|
2619
|
-
* @param tab The tab to navigate to, if applicable.
|
|
2620
|
-
*/
|
|
2621
|
-
const handleNavigate = (path, routeAction, routeDirection, routeAnimation, routeOptions, tab) => {
|
|
2622
|
-
var _a;
|
|
2623
|
-
const normalizedRouteDirection = routeAction === 'push' && routeDirection === undefined ? 'forward' : routeDirection;
|
|
2624
|
-
// When navigating from tabs context, we need to determine if the destination
|
|
2625
|
-
// is also within tabs. If not, we should clear the tab context.
|
|
2626
|
-
let navigationTab = tab;
|
|
2627
|
-
// If no explicit tab is provided and we're in a tab context,
|
|
2628
|
-
// check if the destination path is outside of the current tab context
|
|
2629
|
-
if (!tab && currentTab.current && path) {
|
|
2630
|
-
// Get the current route info to understand where we are
|
|
2631
|
-
const currentRoute = locationHistory.current.current();
|
|
2632
|
-
// If we're navigating from a tab route to a completely different path structure,
|
|
2633
|
-
// we should clear the tab context. This is a simplified check that assumes
|
|
2634
|
-
// tab routes share a common parent path.
|
|
2635
|
-
if (currentRoute && currentRoute.pathname) {
|
|
2636
|
-
// Extract the base tab path (e.g., /routing/tabs from /routing/tabs/home)
|
|
2637
|
-
const tabBaseMatch = currentRoute.pathname.match(/^(.*\/tabs)/);
|
|
2638
|
-
if (tabBaseMatch) {
|
|
2639
|
-
const tabBasePath = tabBaseMatch[1];
|
|
2640
|
-
// If the new path doesn't start with the tab base path, we're leaving tabs
|
|
2641
|
-
if (!path.startsWith(tabBasePath)) {
|
|
2642
|
-
currentTab.current = undefined;
|
|
2643
|
-
navigationTab = undefined;
|
|
2644
|
-
}
|
|
2645
|
-
else {
|
|
2646
|
-
// Still within tabs, preserve the tab context
|
|
2647
|
-
navigationTab = currentTab.current;
|
|
2648
|
-
}
|
|
2649
|
-
}
|
|
2650
|
-
}
|
|
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 || ''));
|
|
2651
828
|
}
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
addViewItem: viewStack.current.add,
|
|
2668
|
-
unMountViewItem: viewStack.current.remove,
|
|
2669
|
-
};
|
|
2670
|
-
return (React.createElement(RouteManagerContext.Provider, { value: routeMangerContextValue },
|
|
2671
|
-
React.createElement(NavManager, { ionRoute: IonRouteInner, ionRedirect: {}, stackManager: StackManager, routeInfo: routeInfo, onNativeBack: handleNativeBack, onNavigateBack: handleNavigateBack, onNavigate: handleNavigate, onSetCurrentTab: handleSetCurrentTab, onChangeTab: handleChangeTab, onResetTab: handleResetTab, locationHistory: locationHistory.current }, children)));
|
|
2672
|
-
};
|
|
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);
|
|
836
|
+
}
|
|
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);
|
|
2673
844
|
IonRouter.displayName = 'IonRouter';
|
|
2674
845
|
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
* `useLocation` and `useNavigationType` are called within the valid
|
|
2684
|
-
* context of a `<BrowserRouter>`.
|
|
2685
|
-
*
|
|
2686
|
-
* It was split from `IonReactRouter` because these hooks must be
|
|
2687
|
-
* descendants of a `<Router>` component, which `BrowserRouter` provides.
|
|
2688
|
-
*/
|
|
2689
|
-
const RouterContent$2 = ({ children }) => {
|
|
2690
|
-
const location = useLocation();
|
|
2691
|
-
const navigationType = useNavigationType();
|
|
2692
|
-
const historyListenHandler = useRef();
|
|
2693
|
-
const registerHistoryListener = useCallback((cb) => {
|
|
2694
|
-
historyListenHandler.current = cb;
|
|
2695
|
-
}, []);
|
|
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
|
+
}
|
|
2696
854
|
/**
|
|
2697
|
-
*
|
|
2698
|
-
*
|
|
2699
|
-
*
|
|
2700
|
-
*
|
|
2701
|
-
*
|
|
2702
|
-
*
|
|
2703
|
-
*
|
|
2704
|
-
* @param loc The current browser history location object.
|
|
2705
|
-
* @param act The type of navigation action ('PUSH', 'POP', or
|
|
2706
|
-
* '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.
|
|
2707
861
|
*/
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
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);
|
|
2711
867
|
}
|
|
2712
|
-
}
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
}
|
|
2716
|
-
|
|
2717
|
-
};
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
};
|
|
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
|
+
}
|
|
2723
878
|
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
const navigationType = useNavigationType$1();
|
|
2732
|
-
const historyListenHandler = useRef();
|
|
2733
|
-
const registerHistoryListener = (cb) => {
|
|
2734
|
-
historyListenHandler.current = cb;
|
|
2735
|
-
};
|
|
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
|
+
}
|
|
2736
886
|
/**
|
|
2737
|
-
*
|
|
2738
|
-
*
|
|
2739
|
-
*
|
|
2740
|
-
*
|
|
2741
|
-
*
|
|
2742
|
-
*
|
|
2743
|
-
*
|
|
2744
|
-
* @param location The current browser history location object.
|
|
2745
|
-
* @param action The type of navigation action ('PUSH', 'POP', or
|
|
2746
|
-
* '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.
|
|
2747
893
|
*/
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
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);
|
|
2751
899
|
}
|
|
2752
|
-
}
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
}
|
|
2756
|
-
|
|
2757
|
-
};
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
};
|
|
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
|
+
}
|
|
2763
910
|
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
const registerHistoryListener = (cb) => {
|
|
2773
|
-
historyListenHandler.current = cb;
|
|
2774
|
-
};
|
|
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
|
+
}
|
|
2775
919
|
/**
|
|
2776
|
-
*
|
|
2777
|
-
*
|
|
2778
|
-
*
|
|
2779
|
-
*
|
|
2780
|
-
*
|
|
2781
|
-
*
|
|
2782
|
-
*
|
|
2783
|
-
* @param location The current browser history location object.
|
|
2784
|
-
* @param action The type of navigation action ('PUSH', 'POP', or
|
|
2785
|
-
* '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.
|
|
2786
926
|
*/
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
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);
|
|
2790
932
|
}
|
|
2791
|
-
}
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
}
|
|
2795
|
-
|
|
2796
|
-
};
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
};
|
|
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
|
+
}
|
|
2802
943
|
|
|
2803
944
|
export { IonReactHashRouter, IonReactMemoryRouter, IonReactRouter };
|
|
2804
945
|
//# sourceMappingURL=index.js.map
|