mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-04-28 12:25:21 +08:00
490 lines
13 KiB
TypeScript
490 lines
13 KiB
TypeScript
import { nanoid } from 'nanoid/non-secure';
|
|
import BaseRouter from './BaseRouter';
|
|
import type {
|
|
NavigationState,
|
|
CommonNavigationAction,
|
|
Router,
|
|
DefaultRouterOptions,
|
|
Route,
|
|
ParamListBase,
|
|
} from './types';
|
|
|
|
export type StackActionType =
|
|
| {
|
|
type: 'REPLACE';
|
|
payload: { name: string; key?: string | undefined; params?: object };
|
|
source?: string;
|
|
target?: string;
|
|
}
|
|
| {
|
|
type: 'PUSH';
|
|
payload: { name: string; key?: string | undefined; params?: object };
|
|
source?: string;
|
|
target?: string;
|
|
}
|
|
| {
|
|
type: 'POP';
|
|
payload: { count: number };
|
|
source?: string;
|
|
target?: string;
|
|
}
|
|
| {
|
|
type: 'POP_TO_TOP';
|
|
source?: string;
|
|
target?: string;
|
|
};
|
|
|
|
export type StackRouterOptions = DefaultRouterOptions;
|
|
|
|
export type StackNavigationState<
|
|
ParamList extends ParamListBase
|
|
> = NavigationState<ParamList> & {
|
|
/**
|
|
* Type of the router, in this case, it's stack.
|
|
*/
|
|
type: 'stack';
|
|
};
|
|
|
|
export type StackActionHelpers<ParamList extends ParamListBase> = {
|
|
/**
|
|
* Replace the current route with a new one.
|
|
*
|
|
* @param name Route name of the new route.
|
|
* @param [params] Params object for the new route.
|
|
*/
|
|
replace<RouteName extends keyof ParamList>(
|
|
...args: undefined extends ParamList[RouteName]
|
|
? [RouteName] | [RouteName, ParamList[RouteName]]
|
|
: [RouteName, ParamList[RouteName]]
|
|
): void;
|
|
|
|
/**
|
|
* Push a new screen onto the stack.
|
|
*
|
|
* @param name Name of the route for the tab.
|
|
* @param [params] Params object for the route.
|
|
*/
|
|
push<RouteName extends keyof ParamList>(
|
|
...args: undefined extends ParamList[RouteName]
|
|
? [RouteName] | [RouteName, ParamList[RouteName]]
|
|
: [RouteName, ParamList[RouteName]]
|
|
): void;
|
|
|
|
/**
|
|
* Pop a screen from the stack.
|
|
*/
|
|
pop(count?: number): void;
|
|
|
|
/**
|
|
* Pop to the first route in the stack, dismissing all other screens.
|
|
*/
|
|
popToTop(): void;
|
|
};
|
|
|
|
export const StackActions = {
|
|
replace(name: string, params?: object): StackActionType {
|
|
return { type: 'REPLACE', payload: { name, params } };
|
|
},
|
|
push(name: string, params?: object): StackActionType {
|
|
return { type: 'PUSH', payload: { name, params } };
|
|
},
|
|
pop(count: number = 1): StackActionType {
|
|
return { type: 'POP', payload: { count } };
|
|
},
|
|
popToTop(): StackActionType {
|
|
return { type: 'POP_TO_TOP' };
|
|
},
|
|
};
|
|
|
|
export default function StackRouter(options: StackRouterOptions) {
|
|
const router: Router<
|
|
StackNavigationState<ParamListBase>,
|
|
CommonNavigationAction | StackActionType
|
|
> = {
|
|
...BaseRouter,
|
|
|
|
type: 'stack',
|
|
|
|
getInitialState({ routeNames, routeParamList }) {
|
|
const initialRouteName =
|
|
options.initialRouteName !== undefined &&
|
|
routeNames.includes(options.initialRouteName)
|
|
? options.initialRouteName
|
|
: routeNames[0];
|
|
|
|
return {
|
|
stale: false,
|
|
type: 'stack',
|
|
key: `stack-${nanoid()}`,
|
|
index: 0,
|
|
routeNames,
|
|
routes: [
|
|
{
|
|
key: `${initialRouteName}-${nanoid()}`,
|
|
name: initialRouteName,
|
|
params: routeParamList[initialRouteName],
|
|
},
|
|
],
|
|
};
|
|
},
|
|
|
|
getRehydratedState(partialState, { routeNames, routeParamList }) {
|
|
let state = partialState;
|
|
|
|
if (state.stale === false) {
|
|
return state;
|
|
}
|
|
|
|
const routes = state.routes
|
|
.filter((route) => routeNames.includes(route.name))
|
|
.map(
|
|
(route) =>
|
|
({
|
|
...route,
|
|
key: route.key || `${route.name}-${nanoid()}`,
|
|
params:
|
|
routeParamList[route.name] !== undefined
|
|
? {
|
|
...routeParamList[route.name],
|
|
...route.params,
|
|
}
|
|
: route.params,
|
|
} as Route<string>)
|
|
);
|
|
|
|
if (routes.length === 0) {
|
|
const initialRouteName =
|
|
options.initialRouteName !== undefined
|
|
? options.initialRouteName
|
|
: routeNames[0];
|
|
|
|
routes.push({
|
|
key: `${initialRouteName}-${nanoid()}`,
|
|
name: initialRouteName,
|
|
params: routeParamList[initialRouteName],
|
|
});
|
|
}
|
|
|
|
return {
|
|
stale: false,
|
|
type: 'stack',
|
|
key: `stack-${nanoid()}`,
|
|
index: routes.length - 1,
|
|
routeNames,
|
|
routes,
|
|
};
|
|
},
|
|
|
|
getStateForRouteNamesChange(state, { routeNames, routeParamList }) {
|
|
const routes = state.routes.filter((route) =>
|
|
routeNames.includes(route.name)
|
|
);
|
|
|
|
if (routes.length === 0) {
|
|
const initialRouteName =
|
|
options.initialRouteName !== undefined &&
|
|
routeNames.includes(options.initialRouteName)
|
|
? options.initialRouteName
|
|
: routeNames[0];
|
|
|
|
routes.push({
|
|
key: `${initialRouteName}-${nanoid()}`,
|
|
name: initialRouteName,
|
|
params: routeParamList[initialRouteName],
|
|
});
|
|
}
|
|
|
|
return {
|
|
...state,
|
|
routeNames,
|
|
routes,
|
|
index: Math.min(state.index, routes.length - 1),
|
|
};
|
|
},
|
|
|
|
getStateForRouteFocus(state, key) {
|
|
const index = state.routes.findIndex((r) => r.key === key);
|
|
|
|
if (index === -1 || index === state.index) {
|
|
return state;
|
|
}
|
|
|
|
return {
|
|
...state,
|
|
index,
|
|
routes: state.routes.slice(0, index + 1),
|
|
};
|
|
},
|
|
|
|
getStateForAction(state, action, options) {
|
|
const { routeParamList } = options;
|
|
|
|
switch (action.type) {
|
|
case 'REPLACE': {
|
|
const index =
|
|
action.target === state.key && action.source
|
|
? state.routes.findIndex((r) => r.key === action.source)
|
|
: state.index;
|
|
|
|
if (index === -1) {
|
|
return null;
|
|
}
|
|
|
|
const { name, key, params } = action.payload;
|
|
|
|
if (!state.routeNames.includes(name)) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
...state,
|
|
routes: state.routes.map((route, i) =>
|
|
i === index
|
|
? {
|
|
key: key !== undefined ? key : `${name}-${nanoid()}`,
|
|
name,
|
|
params:
|
|
routeParamList[name] !== undefined
|
|
? {
|
|
...routeParamList[name],
|
|
...params,
|
|
}
|
|
: params,
|
|
}
|
|
: route
|
|
),
|
|
};
|
|
}
|
|
|
|
case 'PUSH':
|
|
if (state.routeNames.includes(action.payload.name)) {
|
|
const getId = options.routeGetIdList[action.payload.name];
|
|
const id = getId?.({ params: action.payload.params });
|
|
|
|
const route =
|
|
action.payload.name && action.payload.key
|
|
? state.routes.find(
|
|
(route) =>
|
|
route.name === action.payload.name &&
|
|
route.key === action.payload.key
|
|
)
|
|
: id
|
|
? state.routes.find(
|
|
(route) =>
|
|
route.name === action.payload.name &&
|
|
id === getId?.({ params: route.params })
|
|
)
|
|
: undefined;
|
|
|
|
let routes: Route<string>[];
|
|
|
|
if (route) {
|
|
routes = state.routes.filter((r) => r.key !== route.key);
|
|
routes.push({
|
|
...route,
|
|
params:
|
|
action.payload.params !== undefined
|
|
? {
|
|
...route.params,
|
|
...action.payload.params,
|
|
}
|
|
: route.params,
|
|
});
|
|
} else {
|
|
routes = [
|
|
...state.routes,
|
|
{
|
|
key:
|
|
action.payload.key ?? `${action.payload.name}-${nanoid()}`,
|
|
name: action.payload.name,
|
|
params:
|
|
routeParamList[action.payload.name] !== undefined
|
|
? {
|
|
...routeParamList[action.payload.name],
|
|
...action.payload.params,
|
|
}
|
|
: action.payload.params,
|
|
},
|
|
];
|
|
}
|
|
|
|
return {
|
|
...state,
|
|
index: routes.length - 1,
|
|
routes,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
|
|
case 'POP': {
|
|
const index =
|
|
action.target === state.key && action.source
|
|
? state.routes.findIndex((r) => r.key === action.source)
|
|
: state.index;
|
|
|
|
if (index > 0) {
|
|
const count = Math.max(index - action.payload.count + 1, 1);
|
|
const routes = state.routes
|
|
.slice(0, count)
|
|
.concat(state.routes.slice(index + 1));
|
|
|
|
return {
|
|
...state,
|
|
index: routes.length - 1,
|
|
routes,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
case 'POP_TO_TOP':
|
|
return router.getStateForAction(
|
|
state,
|
|
{
|
|
type: 'POP',
|
|
payload: { count: state.routes.length - 1 },
|
|
},
|
|
options
|
|
);
|
|
|
|
case 'NAVIGATE':
|
|
if (
|
|
action.payload.name !== undefined &&
|
|
!state.routeNames.includes(action.payload.name)
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
if (action.payload.key || action.payload.name) {
|
|
// If the route already exists, navigate to that
|
|
let index = -1;
|
|
|
|
const getId =
|
|
// `getId` and `key` can't be used together
|
|
action.payload.key === undefined &&
|
|
action.payload.name !== undefined
|
|
? options.routeGetIdList[action.payload.name]
|
|
: undefined;
|
|
const id = getId?.({ params: action.payload.params });
|
|
|
|
if (id) {
|
|
index = state.routes.findIndex(
|
|
(route) =>
|
|
route.name === action.payload.name &&
|
|
id === getId?.({ params: route.params })
|
|
);
|
|
} else if (
|
|
(state.routes[state.index].name === action.payload.name &&
|
|
action.payload.key === undefined) ||
|
|
state.routes[state.index].key === action.payload.key
|
|
) {
|
|
index = state.index;
|
|
} else {
|
|
for (let i = state.routes.length - 1; i >= 0; i--) {
|
|
if (
|
|
(state.routes[i].name === action.payload.name &&
|
|
action.payload.key === undefined) ||
|
|
state.routes[i].key === action.payload.key
|
|
) {
|
|
index = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (
|
|
index === -1 &&
|
|
action.payload.key &&
|
|
action.payload.name === undefined
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
if (index === -1 && action.payload.name !== undefined) {
|
|
const routes = [
|
|
...state.routes,
|
|
{
|
|
key:
|
|
action.payload.key ?? `${action.payload.name}-${nanoid()}`,
|
|
name: action.payload.name,
|
|
params:
|
|
routeParamList[action.payload.name] !== undefined
|
|
? {
|
|
...routeParamList[action.payload.name],
|
|
...action.payload.params,
|
|
}
|
|
: action.payload.params,
|
|
},
|
|
];
|
|
|
|
return {
|
|
...state,
|
|
routes,
|
|
index: routes.length - 1,
|
|
};
|
|
}
|
|
|
|
const route = state.routes[index];
|
|
|
|
let params;
|
|
|
|
if (action.payload.merge === false) {
|
|
params =
|
|
routeParamList[route.name] !== undefined
|
|
? {
|
|
...routeParamList[route.name],
|
|
...action.payload.params,
|
|
}
|
|
: action.payload.params;
|
|
} else {
|
|
params = action.payload.params
|
|
? {
|
|
...route.params,
|
|
...action.payload.params,
|
|
}
|
|
: route.params;
|
|
}
|
|
|
|
return {
|
|
...state,
|
|
index,
|
|
routes: [
|
|
...state.routes.slice(0, index),
|
|
params !== route.params
|
|
? { ...route, params }
|
|
: state.routes[index],
|
|
],
|
|
};
|
|
}
|
|
|
|
return null;
|
|
|
|
case 'GO_BACK':
|
|
if (state.index > 0) {
|
|
return router.getStateForAction(
|
|
state,
|
|
{
|
|
type: 'POP',
|
|
payload: { count: 1 },
|
|
target: action.target,
|
|
source: action.source,
|
|
},
|
|
options
|
|
);
|
|
}
|
|
|
|
return null;
|
|
|
|
default:
|
|
return BaseRouter.getStateForAction(state, action);
|
|
}
|
|
},
|
|
|
|
actionCreators: StackActions,
|
|
};
|
|
|
|
return router;
|
|
}
|