import pathToRegexp from 'path-to-regexp'; import NavigationActions from '../NavigationActions'; import createConfigGetter from './createConfigGetter'; import getScreenForRouteName from './getScreenForRouteName'; import StateUtils from '../StateUtils'; import validateRouteConfigMap from './validateRouteConfigMap'; import getScreenConfigDeprecated from './getScreenConfigDeprecated'; import invariant from '../utils/invariant'; import { generateKey } from './KeyGenerator'; function isEmpty(obj) { if (!obj) return true; for (let key in obj) { return false; } return true; } export default (routeConfigs, stackConfig = {}) => { // Fail fast on invalid route definitions validateRouteConfigMap(routeConfigs); const childRouters = {}; const routeNames = Object.keys(routeConfigs); // Loop through routes and find child routers routeNames.forEach(routeName => { const screen = getScreenForRouteName(routeConfigs, routeName); if (screen && screen.router) { // If it has a router it's a navigator. childRouters[routeName] = screen.router; } else { // If it doesn't have router it's an ordinary React component. childRouters[routeName] = null; } }); const { initialRouteParams } = stackConfig; const initialRouteName = stackConfig.initialRouteName || routeNames[0]; const initialChildRouter = childRouters[initialRouteName]; const pathsByRouteNames = { ...stackConfig.paths } || {}; let paths = []; // Build paths for each route routeNames.forEach(routeName => { let pathPattern = pathsByRouteNames[routeName] || routeConfigs[routeName].path; let matchExact = !!pathPattern && !childRouters[routeName]; if (pathPattern === undefined) { pathPattern = routeName; } const keys = []; let re, toPath, priority; if (typeof pathPattern === 'string') { // pathPattern may be either a string or a regexp object according to path-to-regexp docs. re = pathToRegexp(pathPattern, keys); toPath = pathToRegexp.compile(pathPattern); priority = 0; } else { // for wildcard match re = pathToRegexp('*', keys); toPath = () => ''; matchExact = true; priority = -1; } if (!matchExact) { const wildcardRe = pathToRegexp(`${pathPattern}/*`, keys); re = new RegExp(`(?:${re.source})|(?:${wildcardRe.source})`); } pathsByRouteNames[routeName] = { re, keys, toPath, priority }; }); paths = Object.entries(pathsByRouteNames); /* $FlowFixMe */ paths.sort((a: [string, *], b: [string, *]) => b[1].priority - a[1].priority); return { getComponentForState(state) { const activeChildRoute = state.routes[state.index]; const { routeName } = activeChildRoute; if (childRouters[routeName]) { return childRouters[routeName].getComponentForState(activeChildRoute); } return getScreenForRouteName(routeConfigs, routeName); }, getComponentForRouteName(routeName) { return getScreenForRouteName(routeConfigs, routeName); }, getStateForAction(action, state) { // Set up the initial state if needed if (!state) { let route = {}; if ( action.type === NavigationActions.NAVIGATE && childRouters[action.routeName] !== undefined ) { return { isTransitioning: false, index: 0, routes: [ { routeName: action.routeName, params: action.params, type: undefined, key: `Init-${generateKey()}`, }, ], }; } if (initialChildRouter) { route = initialChildRouter.getStateForAction( NavigationActions.navigate({ routeName: initialRouteName, params: initialRouteParams, }) ); } const params = (route.params || action.params || initialRouteParams) && { ...(route.params || {}), ...(action.params || {}), ...(initialRouteParams || {}), }; route = { ...route, routeName: initialRouteName, key: `Init-${generateKey()}`, ...(params ? { params } : {}), }; // eslint-disable-next-line no-param-reassign state = { isTransitioning: false, index: 0, routes: [route], }; } // Check if a child scene wants to handle the action as long as it is not a reset to the root stack if (action.type !== NavigationActions.RESET || action.key !== null) { const keyIndex = action.key ? StateUtils.indexOf(state, action.key) : -1; const childIndex = keyIndex >= 0 ? keyIndex : state.index; const childRoute = state.routes[childIndex]; invariant( childRoute, `StateUtils erroneously thought index ${childIndex} exists` ); const childRouter = childRouters[childRoute.routeName]; if (childRouter) { const route = childRouter.getStateForAction(action, childRoute); if (route === null) { return state; } if (route && route !== childRoute) { return StateUtils.replaceAt(state, childRoute.key, route); } } } // Handle explicit push navigation action if ( action.type === NavigationActions.NAVIGATE && childRouters[action.routeName] !== undefined ) { const childRouter = childRouters[action.routeName]; let route; // The key may be provided for pushing, or to navigate back to the key if (action.key) { const lastRouteIndex = state.routes.findIndex( r => r.key === action.key ); if (lastRouteIndex !== -1) { // If index is unchanged and params are not being set, leave state identity intact if (state.index === lastRouteIndex && !action.params) { return state; } const routes = [...state.routes]; // Apply params if provided, otherwise leave route identity intact if (action.params) { const route = state.routes.find(r => r.key === action.key); routes[lastRouteIndex] = { ...route, params: { ...route.params, ...action.params, }, }; } // Return state with new index. Change isTransitioning only if index has changed return { ...state, isTransitioning: state.index !== lastRouteIndex ? action.immediate !== true : undefined, index: lastRouteIndex, routes, }; } } const key = action.key || generateKey(); if (childRouter) { const childAction = action.action || NavigationActions.init({ params: action.params }); route = { params: action.params, ...childRouter.getStateForAction(childAction), key, routeName: action.routeName, }; } else { route = { params: action.params, key, routeName: action.routeName, }; } return { ...StateUtils.push(state, route), isTransitioning: action.immediate !== true, }; } if ( action.type === NavigationActions.COMPLETE_TRANSITION && state.isTransitioning ) { return { ...state, isTransitioning: false, }; } // Handle navigation to other child routers that are not yet pushed if (action.type === NavigationActions.NAVIGATE) { const childRouterNames = Object.keys(childRouters); for (let i = 0; i < childRouterNames.length; i++) { const childRouterName = childRouterNames[i]; const childRouter = childRouters[childRouterName]; if (childRouter) { // For each child router, start with a blank state const initChildRoute = childRouter.getStateForAction( NavigationActions.init() ); // Then check to see if the router handles our navigate action const navigatedChildRoute = childRouter.getStateForAction( action, initChildRoute ); let routeToPush = null; if (navigatedChildRoute === null) { // Push the route if the router has 'handled' the action and returned null routeToPush = initChildRoute; } else if (navigatedChildRoute !== initChildRoute) { // Push the route if the state has changed in response to this navigation routeToPush = navigatedChildRoute; } if (routeToPush) { return StateUtils.push(state, { ...routeToPush, key: generateKey(), routeName: childRouterName, }); } } } } if (action.type === NavigationActions.SET_PARAMS) { const key = action.key; const lastRoute = state.routes.find(route => route.key === key); if (lastRoute) { const params = { ...lastRoute.params, ...action.params, }; const routes = [...state.routes]; routes[state.routes.indexOf(lastRoute)] = { ...lastRoute, params, }; return { ...state, routes, }; } } if (action.type === NavigationActions.RESET) { // Only handle reset actions that are unspecified or match this state key if (action.key != null && action.key != state.key) { // Deliberately use != instead of !== so we can match null with // undefined on either the state or the action return state; } const resetAction = action; return { ...state, routes: resetAction.actions.map(childAction => { const router = childRouters[childAction.routeName]; if (router) { return { ...childAction, ...router.getStateForAction(childAction), routeName: childAction.routeName, key: generateKey(), }; } const route = { ...childAction, key: generateKey(), }; delete route.type; return route; }), index: action.index, }; } if ( action.type === NavigationActions.BACK || action.type === NavigationActions.POP ) { const { key, n, immediate } = action; let backRouteIndex = state.index; if (action.type === NavigationActions.POP && n != null) { // determine the index to go back *from*. In this case, n=1 means to go // back from state.index, as if it were a normal "BACK" action backRouteIndex = Math.max(1, state.index - n + 1); } else if (key) { const backRoute = state.routes.find(route => route.key === key); backRouteIndex = state.routes.indexOf(backRoute); } if (backRouteIndex > 0) { return { ...state, routes: state.routes.slice(0, backRouteIndex), index: backRouteIndex - 1, isTransitioning: immediate !== true, }; } } return state; }, getPathAndParamsForState(state) { const route = state.routes[state.index]; const routeName = route.routeName; const screen = getScreenForRouteName(routeConfigs, routeName); const subPath = pathsByRouteNames[routeName].toPath(route.params); let path = subPath; let params = route.params; if (screen && screen.router) { const stateRoute = route; // If it has a router it's a navigator. // If it doesn't have router it's an ordinary React component. const child = screen.router.getPathAndParamsForState(stateRoute); path = subPath ? `${subPath}/${child.path}` : child.path; params = child.params ? { ...params, ...child.params } : params; } return { path, params, }; }, getActionForPathAndParams(pathToResolve, inputParams) { // If the path is empty (null or empty string) // just return the initial route action if (!pathToResolve) { return NavigationActions.navigate({ routeName: initialRouteName, }); } const [pathNameToResolve, queryString] = pathToResolve.split('?'); // Attempt to match `pathNameToResolve` with a route in this router's // routeConfigs let matchedRouteName; let pathMatch; let pathMatchKeys; // eslint-disable-next-line no-restricted-syntax for (const [routeName, path] of paths) { const { re, keys } = path; pathMatch = re.exec(pathNameToResolve); if (pathMatch && pathMatch.length) { pathMatchKeys = keys; matchedRouteName = routeName; break; } } // We didn't match -- return null if (!matchedRouteName) { // If the path is empty (null or empty string) // just return the initial route action if (!pathToResolve) { return NavigationActions.navigate({ routeName: initialRouteName, }); } return null; } // Determine nested actions: // If our matched route for this router is a child router, // get the action for the path AFTER the matched path for this // router let nestedAction; let nestedQueryString = queryString ? '?' + queryString : ''; if (childRouters[matchedRouteName]) { nestedAction = childRouters[matchedRouteName].getActionForPathAndParams( pathMatch.slice(pathMatchKeys.length).join('/') + nestedQueryString ); if (!nestedAction) { return null; } } // reduce the items of the query string. any query params may // be overridden by path params const queryParams = !isEmpty(inputParams) ? inputParams : (queryString || '').split('&').reduce((result, item) => { if (item !== '') { const nextResult = result || {}; const [key, value] = item.split('='); nextResult[key] = value; return nextResult; } return result; }, null); // reduce the matched pieces of the path into the params // of the route. `params` is null if there are no params. const params = pathMatch.slice(1).reduce((result, matchResult, i) => { const key = pathMatchKeys[i]; if (key.asterisk || !key) { return result; } const nextResult = result || {}; const paramName = key.name; nextResult[paramName] = matchResult; return nextResult; }, queryParams); return NavigationActions.navigate({ routeName: matchedRouteName, ...(params ? { params } : {}), ...(nestedAction ? { action: nestedAction } : {}), }); }, getScreenOptions: createConfigGetter( routeConfigs, stackConfig.navigationOptions ), getScreenConfig: getScreenConfigDeprecated, }; };