mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
refactor(react-router): extract helper functions from computeParentPath
This commit is contained in:
@@ -125,6 +125,103 @@ interface ComputeParentPathOptions {
|
||||
hasWildcardRoute: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if any route matches as a specific (non-wildcard, non-index) route.
|
||||
*/
|
||||
const findSpecificMatch = (routeChildren: React.ReactElement[], remainingPath: string): boolean => {
|
||||
return routeChildren.some(
|
||||
(route) => isSpecificRouteMatch(route, remainingPath) || matchesEmbeddedWildcardRoute(route, remainingPath)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if any specific route could plausibly match the remaining path.
|
||||
* Used to determine if we should fall back to a wildcard match.
|
||||
*/
|
||||
const couldSpecificRouteMatch = (routeChildren: React.ReactElement[], remainingPath: string): boolean => {
|
||||
const remainingFirstSegment = remainingPath.split('/')[0];
|
||||
return routeChildren.some((route) => {
|
||||
const routePath = route.props.path as string | undefined;
|
||||
if (!routePath || routePath === '*' || routePath === '/*') return false;
|
||||
if (route.props.index) return false;
|
||||
|
||||
const routeFirstSegment = routePath.split('/')[0].replace(/[*:]/g, '');
|
||||
if (!routeFirstSegment) return false;
|
||||
|
||||
// Check for prefix overlap (either direction)
|
||||
return (
|
||||
routeFirstSegment.startsWith(remainingFirstSegment.slice(0, 3)) ||
|
||||
remainingFirstSegment.startsWith(routeFirstSegment.slice(0, 3))
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks for index route match when remaining path is empty.
|
||||
* Index routes only match at the outlet's mount path level.
|
||||
*/
|
||||
const checkIndexMatch = (
|
||||
parentPath: string,
|
||||
remainingPath: string,
|
||||
hasIndexRoute: boolean,
|
||||
outletMountPath: string | undefined
|
||||
): string | undefined => {
|
||||
if ((remainingPath === '' || remainingPath === '/') && hasIndexRoute) {
|
||||
if (outletMountPath) {
|
||||
// Index should only match at the existing mount path
|
||||
return parentPath === outletMountPath ? parentPath : undefined;
|
||||
}
|
||||
// No mount path yet - this would establish it
|
||||
return parentPath;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines the best parent path from the available matches.
|
||||
* Priority: specific > wildcard > index
|
||||
*/
|
||||
const selectBestMatch = (
|
||||
specificMatch: string | undefined,
|
||||
wildcardMatch: string | undefined,
|
||||
indexMatch: string | undefined
|
||||
): string | undefined => {
|
||||
return specificMatch ?? wildcardMatch ?? indexMatch;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles outlets with only absolute routes by computing their common prefix.
|
||||
*/
|
||||
const computeAbsoluteRoutesParentPath = (
|
||||
routeChildren: React.ReactElement[],
|
||||
currentPathname: string,
|
||||
outletMountPath: string | undefined
|
||||
): ParentPathResult | undefined => {
|
||||
const absolutePathRoutes = routeChildren.filter((route) => {
|
||||
const path = route.props.path;
|
||||
return path && path.startsWith('/');
|
||||
});
|
||||
|
||||
if (absolutePathRoutes.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const absolutePaths = absolutePathRoutes.map((r) => r.props.path as string);
|
||||
const commonPrefix = computeCommonPrefix(absolutePaths);
|
||||
|
||||
if (!commonPrefix || commonPrefix === '/') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const newOutletMountPath = outletMountPath || commonPrefix;
|
||||
|
||||
if (!currentPathname.startsWith(commonPrefix)) {
|
||||
return { parentPath: undefined, outletMountPath: newOutletMountPath };
|
||||
}
|
||||
|
||||
return { parentPath: commonPrefix, outletMountPath: newOutletMountPath };
|
||||
};
|
||||
|
||||
/**
|
||||
* Computes the parent path for a nested outlet based on the current pathname
|
||||
* and the outlet's route configuration.
|
||||
@@ -139,9 +236,7 @@ export const computeParentPath = (options: ComputeParentPathOptions): ParentPath
|
||||
const { currentPathname, outletMountPath, routeChildren, hasRelativeRoutes, hasIndexRoute, hasWildcardRoute } =
|
||||
options;
|
||||
|
||||
// If this outlet previously established a mount path and the current
|
||||
// pathname is outside of that scope, do not attempt to re-compute a new
|
||||
// parent path.
|
||||
// If pathname is outside the established mount path scope, skip computation
|
||||
if (outletMountPath && !currentPathname.startsWith(outletMountPath)) {
|
||||
return { parentPath: undefined, outletMountPath };
|
||||
}
|
||||
@@ -150,105 +245,49 @@ export const computeParentPath = (options: ComputeParentPathOptions): ParentPath
|
||||
const segments = currentPathname.split('/').filter(Boolean);
|
||||
|
||||
if (segments.length >= 1) {
|
||||
// Find matches at each level, keeping track of the FIRST (shortest) match
|
||||
let firstSpecificMatch: string | undefined = undefined;
|
||||
let firstWildcardMatch: string | undefined = undefined;
|
||||
let indexMatchAtMount: string | undefined = undefined;
|
||||
let firstSpecificMatch: string | undefined;
|
||||
let firstWildcardMatch: string | undefined;
|
||||
let indexMatchAtMount: string | undefined;
|
||||
|
||||
// Start at i = 1 (normal case: strip at least one segment for parent path)
|
||||
// Iterate through path segments to find the shortest matching parent path
|
||||
for (let i = 1; i <= segments.length; i++) {
|
||||
const parentPath = '/' + segments.slice(0, i).join('/');
|
||||
const remainingPath = segments.slice(i).join('/');
|
||||
|
||||
// Check for specific route matches (non-wildcard-only, non-index)
|
||||
// Also check routes with embedded wildcards (e.g., "tab1/*")
|
||||
const hasSpecificMatch = routeChildren.some(
|
||||
(route) => isSpecificRouteMatch(route, remainingPath) || matchesEmbeddedWildcardRoute(route, remainingPath)
|
||||
);
|
||||
if (hasSpecificMatch && !firstSpecificMatch) {
|
||||
// Check for specific route match (highest priority)
|
||||
if (!firstSpecificMatch && findSpecificMatch(routeChildren, remainingPath)) {
|
||||
firstSpecificMatch = parentPath;
|
||||
// Found a specific match - this is our answer for non-index routes
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if wildcard would match this remaining path
|
||||
// Only if remaining is non-empty (wildcard needs something to match)
|
||||
if (remainingPath !== '' && remainingPath !== '/' && hasWildcardRoute && !firstWildcardMatch) {
|
||||
// Check if any specific route could plausibly match this remaining path
|
||||
const remainingFirstSegment = remainingPath.split('/')[0];
|
||||
const couldAnyRouteMatch = routeChildren.some((route) => {
|
||||
const routePath = route.props.path as string | undefined;
|
||||
if (!routePath || routePath === '*' || routePath === '/*') return false;
|
||||
if (route.props.index) return false;
|
||||
|
||||
const routeFirstSegment = routePath.split('/')[0].replace(/[*:]/g, '');
|
||||
if (!routeFirstSegment) return false;
|
||||
|
||||
// Check for prefix overlap (either direction)
|
||||
return (
|
||||
routeFirstSegment.startsWith(remainingFirstSegment.slice(0, 3)) ||
|
||||
remainingFirstSegment.startsWith(routeFirstSegment.slice(0, 3))
|
||||
);
|
||||
});
|
||||
|
||||
// Only save wildcard match if no specific route could match
|
||||
if (!couldAnyRouteMatch) {
|
||||
// Check for wildcard match (only if remaining path is non-empty)
|
||||
const hasNonEmptyRemaining = remainingPath !== '' && remainingPath !== '/';
|
||||
if (!firstWildcardMatch && hasNonEmptyRemaining && hasWildcardRoute) {
|
||||
if (!couldSpecificRouteMatch(routeChildren, remainingPath)) {
|
||||
firstWildcardMatch = parentPath;
|
||||
// Continue looking - might find a specific match at a longer path
|
||||
}
|
||||
}
|
||||
|
||||
// Check for index route match when remaining path is empty
|
||||
// BUT only at the outlet's mount path level
|
||||
if ((remainingPath === '' || remainingPath === '/') && hasIndexRoute) {
|
||||
// Index route matches when current path exactly matches the mount path
|
||||
// If we already have an outletMountPath, index should only match there
|
||||
if (outletMountPath) {
|
||||
if (parentPath === outletMountPath) {
|
||||
indexMatchAtMount = parentPath;
|
||||
}
|
||||
} else {
|
||||
// No mount path set yet - index would establish this as mount path
|
||||
// But only if we haven't found a better match
|
||||
indexMatchAtMount = parentPath;
|
||||
}
|
||||
// Check for index route match
|
||||
const indexMatch = checkIndexMatch(parentPath, remainingPath, hasIndexRoute, outletMountPath);
|
||||
if (indexMatch) {
|
||||
indexMatchAtMount = indexMatch;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: check at root level (i = 0) for embedded wildcard routes.
|
||||
// This handles outlets inside root-level splat routes where routes like
|
||||
// "tab1/*" need to match the full pathname.
|
||||
// Fallback: check root level for embedded wildcard routes (e.g., "tab1/*")
|
||||
if (!firstSpecificMatch) {
|
||||
const fullRemainingPath = segments.join('/');
|
||||
const hasRootLevelMatch = routeChildren.some((route) => matchesEmbeddedWildcardRoute(route, fullRemainingPath));
|
||||
if (hasRootLevelMatch) {
|
||||
if (routeChildren.some((route) => matchesEmbeddedWildcardRoute(route, fullRemainingPath))) {
|
||||
firstSpecificMatch = '/';
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the best parent path:
|
||||
// 1. Specific match (routes like tabs/*, favorites) - highest priority
|
||||
// 2. Wildcard match (route path="*") - catches unmatched segments
|
||||
// 3. Index match - only valid at the outlet's mount point, not deeper
|
||||
let bestPath: string | undefined = undefined;
|
||||
const bestPath = selectBestMatch(firstSpecificMatch, firstWildcardMatch, indexMatchAtMount);
|
||||
|
||||
if (firstSpecificMatch) {
|
||||
bestPath = firstSpecificMatch;
|
||||
} else if (firstWildcardMatch) {
|
||||
bestPath = firstWildcardMatch;
|
||||
} else if (indexMatchAtMount) {
|
||||
// Only use index match if no specific or wildcard matched
|
||||
// This handles the case where pathname exactly matches the mount path
|
||||
bestPath = indexMatchAtMount;
|
||||
}
|
||||
// Establish mount path on first successful match
|
||||
const newOutletMountPath = outletMountPath || bestPath;
|
||||
|
||||
// Store the mount path when we first successfully match a route
|
||||
let newOutletMountPath = outletMountPath;
|
||||
if (!outletMountPath && bestPath) {
|
||||
newOutletMountPath = bestPath;
|
||||
}
|
||||
|
||||
// If we have a mount path, verify the current pathname is within scope
|
||||
if (newOutletMountPath && !currentPathname.startsWith(newOutletMountPath)) {
|
||||
return { parentPath: undefined, outletMountPath: newOutletMountPath };
|
||||
}
|
||||
@@ -257,29 +296,11 @@ export const computeParentPath = (options: ComputeParentPathOptions): ParentPath
|
||||
}
|
||||
}
|
||||
|
||||
// Handle outlets with ONLY absolute routes (no relative routes or index routes)
|
||||
// Compute the common prefix of all absolute routes to determine the outlet's scope
|
||||
// Handle outlets with only absolute routes
|
||||
if (!hasRelativeRoutes && !hasIndexRoute) {
|
||||
const absolutePathRoutes = routeChildren.filter((route) => {
|
||||
const path = route.props.path;
|
||||
return path && path.startsWith('/');
|
||||
});
|
||||
|
||||
if (absolutePathRoutes.length > 0) {
|
||||
const absolutePaths = absolutePathRoutes.map((r) => r.props.path as string);
|
||||
const commonPrefix = computeCommonPrefix(absolutePaths);
|
||||
|
||||
if (commonPrefix && commonPrefix !== '/') {
|
||||
// Set the mount path based on common prefix of absolute routes
|
||||
const newOutletMountPath = outletMountPath || commonPrefix;
|
||||
|
||||
// Check if current pathname is within scope
|
||||
if (!currentPathname.startsWith(commonPrefix)) {
|
||||
return { parentPath: undefined, outletMountPath: newOutletMountPath };
|
||||
}
|
||||
|
||||
return { parentPath: commonPrefix, outletMountPath: newOutletMountPath };
|
||||
}
|
||||
const result = computeAbsoluteRoutesParentPath(routeChildren, currentPathname, outletMountPath);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user