UNPKG

react-native-router-flux

Version:
971 lines (889 loc) 28.9 kB
import React from 'react'; import { Image, Animated, Easing } from 'react-native'; import { createAppContainer, NavigationActions, StackActions } from 'react-navigation'; import { createStackNavigator } from 'react-navigation-stack'; import { createDrawerNavigator, DrawerActions } from 'react-navigation-drawer'; import { createMaterialTopTabNavigator, createBottomTabNavigator } from 'react-navigation-tabs'; import PropTypes from 'prop-types'; import createReducer from './Reducer'; import * as ActionConst from './ActionConst'; import { OnEnter, OnExit, assert } from './Util'; import { LeftButton, RightButton, BackButton } from './NavBar'; import LightboxRenderer from './LightboxRenderer'; import _drawerImage from '../images/menu_burger.png'; import { getActiveState, getParent, getRouteNameByKey } from './State'; import Modal from './Modal'; import Lightbox from './Lightbox'; import Drawer from './Drawer'; import Tabs from './Tabs'; import Overlay from './Overlay'; import OverlayRenderer from './OverlayRenderer'; import createStackNavigatorHOC from './createStackNavigatorHOC'; import createTabNavigatorHOC from './createTabNavigatorHOC'; let RightNavBarButton; let LeftNavBarButton; let BackNavBarButton; let counter = 0; export const actionMap = { [ActionConst.JUMP]: 'jump', [ActionConst.PUSH]: 'push', [ActionConst.REPLACE]: 'replace', [ActionConst.BACK]: 'pop', [ActionConst.BACK_ACTION]: 'pop', [ActionConst.POP_TO]: 'popTo', [ActionConst.REFRESH]: 'refresh', [ActionConst.RESET]: 'reset', [ActionConst.PUSH_OR_POP]: 'push', }; const reservedKeys = [ 'addRef', 'back', 'children', 'create', 'dispatch', 'drawerClose', 'drawerOpen', 'execute', 'left', 'leftButton', 'navBar', 'navigate', 'on', 'onEnter', 'onExit', 'onLeft', 'onRight', 'pop', 'popTo', 'push', 'refresh', 'refs', 'removeRef', 'renderLeftButton', 'renderRightButton', 'renderTitle', 'replace', 'right', 'rightButton', 'run', 'setParams', 'title', ]; const dontInheritKeys = [ 'backToInitial', 'children', 'component', 'contentComponent', 'drawer', 'hideNavBar', 'hideTabBar', 'key', 'lightbox', 'modal', 'navigator', 'navTransparent', 'overlay', 'ref', 'style', 'tabBarComponent', 'tabs', 'title', 'type', ]; function getValue(value, params) { return value instanceof Function ? value(params) : value; } function getProperties(component = {}) { const res = {}; for (const key of reservedKeys) { if (component[key]) { res[key] = component[key]; } } delete res.children; return res; } function createTabBarOptions({ tabBarStyle, activeTintColor, inactiveTintColor, activeBackgroundColor, inactiveBackgroundColor, showLabel, labelStyle, tabStyle, ...props }) { return { ...props, style: tabBarStyle, activeTintColor, inactiveTintColor, activeBackgroundColor, inactiveBackgroundColor, showLabel, labelStyle, tabStyle, }; } function createNavigationOptions(params) { const { type, cardStyle, back, backButtonImage, backButtonTextStyle, backTitle, backTitleEnabled, backToInitial, component, drawerIcon, drawerImage, drawerPosition, getTitle, headerLayoutPreset, headerStyle, headerTitleStyle, hideDrawerButton, hideNavBar, hideTabBar, icon, init, left, leftButton, leftButtonImage, leftButtonTextStyle, leftTitle, navBar, navBarButtonColor, navigationBarStyle, navigationBarTitleImage, navigationBarTitleImageStyle, navTransparent, onLeft, onRight, panHandlers, renderBackButton, renderNavigationBar, renderTitle, right, rightButton, rightButtonImage, rightButtonTextStyle, rightTitle, tabBarIcon, tabBarLabel, title, titleStyle, ...props } = params; const NavBar = renderNavigationBar || navBar; if (component && component.navigationOptions) { return component.navigationOptions; } return ({ navigation, screenProps }) => { const navigationParams = navigation.state.params || {}; const state = { navigation, ...params, ...navigationParams, ...screenProps, }; const res = { animationEnabled: !(type === ActionConst.REPLACE || type === 'replace' || type === ActionConst.RESET || type === 'reset'), ...props, cardStyle: navigationParams.cardStyle || cardStyle, headerBackImage: navigationParams.backButtonImage || backButtonImage, headerBackTitle: getValue(navigationParams.backTitle || backTitle, state), headerBackTitleEnabled: navigationParams.backTitleEnabled || backTitleEnabled, headerLayoutPreset: navigationParams.headerLayoutPreset || headerLayoutPreset, headerLeft: () => getValue(navigationParams.left || left || leftButton || params.renderLeftButton, state), headerRight: () => getValue(navigationParams.right || right || rightButton || params.renderRightButton, state), headerStyle: getValue(navigationParams.headerStyle || headerStyle || navigationBarStyle, state), headerTintColor: navBarButtonColor || props.tintColor || navigationParams.tintColor || navigationParams.headerTintColor, headerTitle: getValue(navigationParams.renderTitle || renderTitle || params.renderTitle, state), headerTitleStyle: headerTitleStyle || titleStyle, title: getValue(navigationParams.title || title || getTitle, state), }; const NavBarFromParams = navigationParams.renderNavigationBar || navigationParams.navBar; if (NavBarFromParams != null) { if (NavBarFromParams) { res.header = data => <NavBarFromParams navigation={navigation} {...state} {...data} />; } } else if (NavBar) { res.header = data => <NavBar navigation={navigation} {...state} {...data} />; } if (typeof navigationParams.panHandlers !== 'undefined') { if (navigationParams.panHandlers === null) { res.gesturesEnabled = false; } } else if (panHandlers === null) { res.gesturesEnabled = false; } if (navigationBarTitleImage) { res.headerTitle = <Image source={navigationBarTitleImage} style={navigationBarTitleImageStyle} />; } if (tabBarLabel) { res.tabBarLabel = tabBarLabel; } if (tabBarIcon || icon) { const Icon = tabBarIcon || icon; res.tabBarIcon = data => <Icon {...state} {...data} />; } const componentData = {}; // copy all component static functions if (component) { for (const key of [ 'onRight', 'onLeft', 'rightButton', 'leftButton', 'leftTitle', 'rightTitle', 'rightButtonImage', 'leftButtonImage', 'rightButtonTextStyle', 'leftButtonTextStyle', 'rightButtonIconStyle', 'leftButtonIconStyle', 'leftButtonTintColor', 'rightButtonTintColor', ]) { if (component[key]) { componentData[key] = component[key]; } } } if ( rightButtonImage || rightTitle || params.renderRightButton || onRight || navigationParams.onRight || navigationParams.rightTitle || navigationParams.rightButtonImage || rightButtonTextStyle || ((drawerImage || drawerIcon) && !hideDrawerButton && drawerPosition === 'right') ) { res.headerRight = () => getValue(navigationParams.right || navigationParams.rightButton || params.renderRightButton, { ...navigationParams, ...screenProps }) || ( <RightNavBarButton navigation={navigation} {...params} {...navigationParams} {...componentData} /> ); } if ( leftButtonImage || backButtonImage || backTitle || leftTitle || params.renderLeftButton || leftButtonTextStyle || renderBackButton || backButtonTextStyle || onLeft || navigationParams.leftTitle || navigationParams.onLeft || navigationParams.leftButtonImage || navigationParams.backButtonImage || navigationParams.backTitle || ((drawerImage || drawerIcon) && !hideDrawerButton && drawerPosition !== 'right') ) { const leftButton = navigationParams.left || navigationParams.leftButton || params.renderLeftButton; res.headerLeft = () => getValue(leftButton, { ...params, ...navigationParams, ...screenProps }) || (((onLeft && (leftTitle || navigationParams.leftTitle || leftButtonImage || navigationParams.leftButtonImage)) || drawerImage || drawerIcon) && ( <LeftNavBarButton navigation={navigation} {...params} {...navigationParams} {...componentData} /> )) || res.headerLeft || (init ? null : (!leftButton && renderBackButton && renderBackButton(state)) || (!leftButton && <BackNavBarButton navigation={navigation} {...state} />)) || null; } if (back) { res.headerLeft = (renderBackButton && renderBackButton(state)) || (() => <BackNavBarButton navigation={navigation} {...state} />); } if (typeof navigationParams.left !== 'undefined' || typeof navigationParams.leftButton !== 'undefined' || typeof navigationParams.renderLeftButton !== 'undefined') { if (navigationParams.left === null || navigationParams.leftButton === null || navigationParams.renderLeftButton === null) { res.headerLeft = null; } } // currect dynamic navigation params has priority over static scene params // but taking them into account only if they are explicitly set (not null or undefined) const routeParams = navigation.state.routes && navigation.state.routes[navigation.state.index].params; if (navigationParams.hideTabBar != null) { if (navigationParams.hideTabBar) { res.tabBarVisible = false; } } else if (hideTabBar) { res.tabBarVisible = false; } else if (routeParams && routeParams.hideTabBar) { res.tabBarVisible = false; } if (navigationParams.hideNavBar != null) { if (navigationParams.hideNavBar) { res.header = null; } } else if (hideNavBar) { res.header = null; } if (navTransparent) { res.headerTransparent = true; res.headerStyle = {}; } if (backToInitial) { const userDefinedTabBarOnPress = res.tabBarOnPress; res.tabBarOnPress = (data) => { if (userDefinedTabBarOnPress) { console.warn('backToInitial and tabBarOnPress were both defined and might cause unexpected navigation behaviors. I hope you know what you are doing ;-)'); userDefinedTabBarOnPress(data); } if (data.navigation && data.navigation.state.index !== 0) { data.navigation.dispatch(StackActions.popToTop()); } else { data.defaultHandler(); } }; } return res; }; } function originalRouteName(routeName) { if (routeName.startsWith('_')) { return routeName.substring(1); } return routeName; } function isStatelessComponent(Component) { return !Component.prototype || typeof Component.prototype.render !== 'function'; } function extendProps(props, store: NavigationStore) { if (!props) { return {}; } const res = { ...props }; for (const transition of Object.keys(props)) { if ( reservedKeys.indexOf(transition) === -1 && transition.startsWith('on') && transition.charAt(2) >= 'A' && transition.charAt(2) <= 'Z' && typeof props[transition] === 'string' ) { if (store[props[transition]]) { res[transition] = params => store[props[transition]](params); } } } return res; } // eslint no-param-reassign: "error" function createWrapper(Component, wrapBy, store: NavigationStore) { if (!Component) { return null; } const wrapper = wrapBy || (props => props); // detect if the component is not functional stateless // not sure if Component can be string-defined ("div") here // may be there is a better way to detect stateless function component, but this should work if (!isStatelessComponent(Component)) { class Wrapped extends React.Component { static propTypes = { navigation: PropTypes.shape().isRequired, }; constructor() { super(); this.onRef = this.onRef.bind(this); } componentDidMount() { const { navigation } = this.props; if (this.ref && navigation && navigation.state && navigation.state.routeName) { store.addRef(originalRouteName(navigation.state.routeName), this.ref); } if (this.ref && this.ref.onEnter) { this.ref.onEnter(navigation && navigation.state); } } componentWillUnmount() { const { navigation } = this.props; if (this.ref && navigation && navigation.state && navigation.state.routeName) { store.deleteRef(originalRouteName(navigation.state.routeName)); } if (this.ref && this.ref.onExit) { this.ref.onExit(navigation && navigation.state); } this.ref = null; } onRef(ref) { this.ref = ref; } render() { const { navigation } = this.props; if (!navigation || !navigation.state) { return <Component ref={this.onRef} {...this.props} />; } return <Component ref={this.onRef} {...this.props} {...extendProps(navigation.state.params, store)} name={navigation.state.routeName} />; } } return wrapper(Wrapped); } // if component is statless function, ref is not supported function StatelessWrapped({ navigation, ...props }) { return <Component {...props} navigation={navigation} {...extendProps(navigation.state.params, store)} name={navigation.state.routeName} />; } StatelessWrapped.propTypes = { navigation: PropTypes.shape().isRequired, }; return wrapper(StatelessWrapped); } function filterParam(data = {}) { if (data.toString() !== '[object Object]') { return { data }; } const proto = (data || {}).constructor.name; // avoid passing React Native parameters if (!data || proto !== 'Object') { return {}; } return data; } function uniteParams(routeName, params) { let res = {}; for (const param of params) { if (param) { res = { ...res, ...filterParam(param) }; } } res.routeName = routeName; return res; } const defaultSuccess = () => {}; const defaultFailure = () => {}; export default class NavigationStore { getStateForAction = null; reducer = null; _navigator = null; externalDispatch = null; externalState = null; prevState = null; externalAction = {}; refs = {}; states = {}; isLogical = {}; currentScene; prevScene; currentParams; onStateChange; set externalState(state) { if (state && this.externalDispatch) { this.onNavigationStateChange(this.state, state, this.externalAction); this.state = state; } } setCustomReducer = (Navigator) => { this.getStateForAction = Navigator.router.getStateForAction; const reducer = createReducer(this); Navigator.router.getStateForAction = (cmd, state) => (this.reducer ? this.reducer(state, cmd) : reducer(state, cmd)); }; onEnterHandler = async (currentScene) => { if (this.states[currentScene]) { const handler = this[currentScene + OnEnter]; const success = this.states[currentScene].success || defaultSuccess; const failure = this.states[currentScene].failure || defaultFailure; if (handler) { try { const res = await handler(this.currentParams, this.state); if (res) { success(res); } else { failure(); } } catch (e) { failure({ error: e.message }); } } } }; onExitHandler = (prevScene) => { if (prevScene) { const exitHandler = this[prevScene + OnExit]; if (exitHandler) { try { const res = exitHandler(this.state); if (res instanceof Promise) { res.then(defaultSuccess, defaultFailure); } } catch (e) { console.error('Error during onExit handler:', e); } } } }; onNavigationStateChange = async (prevState, currentState, action) => { this.state = currentState; this.prevState = prevState; const activeState = getActiveState(this.state); const currentScene = activeState.routeName; this.currentParams = { ...activeState.params, ...action.params }; this.currentScene = currentScene; this.prevScene = this.prevState ? getActiveState(this.prevState).routeName : null; if (this.currentScene !== this.prevScene) { // run onExit for old scene this.onExitHandler(this.prevScene); setTimeout(() => this.dispatch({ type: ActionConst.FOCUS, routeName: this.currentScene, params: this.currentParams, })); this.onEnterHandler(currentScene); } else { const routeName = getRouteNameByKey(this.state, action.key); if (action.type === 'Navigation/DRAWER_OPENED') { this.onEnterHandler(routeName); } else if (action.type === 'Navigation/DRAWER_CLOSED') { this.onExitHandler(routeName); } } if (this.onStateChange) { this.onStateChange(prevState, currentState, action); } }; setTopLevelNavigator = (navigatorRef) => { this._navigator = navigatorRef; }; addRef = (name, ref) => { this.refs[name] = ref; }; deleteRef = (name) => { delete this.refs[name]; }; create = (scene: Scene, params = {}, wrapBy = props => props) => { assert(!Array.isArray(scene), 'Router should contain only one scene, please wrap your scenes with root Scene '); RightNavBarButton = wrapBy(RightButton); LeftNavBarButton = wrapBy(LeftButton); BackNavBarButton = wrapBy(BackButton); const Navigator = this.processScene(scene, params, [], wrapBy); // set initial state this.onNavigationStateChange(null, Navigator.router.getStateForAction(NavigationActions.init()), NavigationActions.init()); this.setCustomReducer(Navigator); return createAppContainer(Navigator); }; createAction = name => (args) => { // console.log(`Transition to state=${name}`); if (this.isLogical[name]) { this[name](args); } else { setTimeout(() => this[name](args)); } }; processScene = (scene: Scene, inheritProps = {}, clones = [], wrapBy) => { assert(scene.props, 'props should be defined'); if (!scene.props.children) { return null; } const res = {}; const order = []; const { navigator, renderer, contentComponent, drawerWidth, drawerLockMode, tabBarPosition, lazy, duration, ...parentProps } = scene.props; let { tabs, modal, lightbox, overlay, drawer, transitionConfig, tabBarComponent, } = parentProps; if (scene.type === Modal) { modal = true; } else if (scene.type === Drawer) { drawer = true; } else if (scene.type === Lightbox) { lightbox = true; } else if (scene.type === Tabs) { tabs = true; } else if (scene.type === Overlay) { overlay = true; } if (duration !== undefined && !transitionConfig) { transitionConfig = () => ({ transitionSpec: { duration, timing: Animated.timing, easing: Easing.step0, }, }); } const commonProps = { ...inheritProps, ...parentProps }; delete commonProps.children; delete commonProps.component; // add inherit props for (const pkey of Object.keys(commonProps)) { if (dontInheritKeys.includes(pkey) && (pkey === 'type' || pkey === 'hideNavBar' || !parentProps[pkey])) { delete commonProps[pkey]; } } if (drawer) { commonProps.drawerImage = commonProps.drawerImage || _drawerImage; } const children = !Array.isArray(parentProps.children) ? [parentProps.children] : [].concat(...parentProps.children); // add clone scenes if (!drawer && !tabs && !overlay) { children.push(...clones); } // add all clones for (const child of children) { if (child && child.props.clone) { if (clones.indexOf(child) === -1) { clones.push(child); } } } let initialRouteName; let initialRouteParams; for (const child of children) { // allow null/false child, useful for conditionals if (!child) { continue; } const key = child.key || `key${(counter += 1)}`; const init = key === children[0].key; assert(reservedKeys.indexOf(key) === -1, `Scene name cannot be reserved word: ${child.key}`); const { component, type = tabs || drawer ? 'jump' : 'push', path, onEnter, onExit, on, failure, success, wrap, initial = false, ...props } = child.props; if (!this.states[key]) { this.states[key] = {}; } for (const transition of Object.keys(props)) { if (reservedKeys.indexOf(transition) === -1 && props[transition] instanceof Function) { this.states[key][transition] = props[transition]; } } delete props.children; if (success) { this.states[key].success = success instanceof Function ? success : this.createAction(success); } if (failure) { this.states[key].failure = failure instanceof Function ? failure : this.createAction(failure); } if (path) { this.states[key].path = path; } // console.log(`KEY ${key} LEGACY {legacy} PATH ${path} DRAWER ${drawer} TABS ${tabs} WRAP ${wrap}`, JSON.stringify(commonProps)); const screen = { screen: createWrapper(component, wrapBy, this) || this.processScene(child, commonProps, clones) || (lightbox && (() => null)), navigationOptions: createNavigationOptions({ ...commonProps, hideNavBar: parentProps.hideNavBar, ...getProperties(component), ...child.props, init, component, }), }; // wrap component inside own navbar for tabs/drawer parent controllers // don't wrap child scenes for custom navigators/renderers let wrapNavBar = drawer || (tabs && !navigator && !renderer) || wrap; if (wrap === false || commonProps.wrap === false) { wrapNavBar = false; } if (component && wrapNavBar) { res[key] = { screen: this.processScene( { key, props: { children: { key: `_${key}`, props: { ...child.props, wrap: false }, }, }, }, commonProps, clones, wrapBy, ), navigationOptions: createNavigationOptions({ ...commonProps, ...child.props, hideNavBar: true, }), }; } else { res[key] = screen; } // a bit of magic, create all 'actions'-shortcuts inside navigationStore props.init = true; if (!this[key]) { this.isLogical[key] = !!component; this[key] = new Function( 'actions', 'props', 'type', `return function ${ key.replace(/\W/g, '_') // eslint-disable-line no-new-func }(params){ actions.execute(type, '${key}', props, params)}`, )(this, { error: '', ...commonProps, ...props }, type); } if ((onEnter || on || (component && component.onEnter)) && !this[key + OnEnter]) { this[key + OnEnter] = onEnter || on || component.onEnter; } if ((onExit || (component && component.onExit)) && !this[key + OnExit]) { this[key + OnExit] = onExit || component.onExit; } order.push(key); if (initial || child.props.initial || !initialRouteName) { initialRouteName = key; initialRouteParams = { ...commonProps, ...props }; } } const mode = modal ? 'modal' : 'card'; const navigationConfig = { lazy, initialRouteName, initialRouteParams, contentComponent, order, ...commonProps, navigationOptions: createNavigationOptions(commonProps), }; if (navigator) { return navigator(res, navigationConfig); } if (renderer) { return tabs ? createTabNavigatorHOC(renderer)(res, navigationConfig) : createStackNavigatorHOC(renderer)(res, navigationConfig); } if (lightbox) { return createStackNavigatorHOC(LightboxRenderer)(res, { mode, initialRouteParams, initialRouteName, ...commonProps, navigationOptions: createNavigationOptions(commonProps), }); } if (tabs) { let createTabNavigator = createMaterialTopTabNavigator; if (tabBarPosition !== 'top') { createTabNavigator = createBottomTabNavigator; } return createTabNavigator(res, { lazy, tabBarComponent, initialRouteName, initialRouteParams, tabBarPosition, order, ...commonProps, tabBarOptions: createTabBarOptions(commonProps), navigationOptions: createNavigationOptions(commonProps), }); } if (drawer) { const config = { initialRouteName, contentComponent, order, drawerOpenRoute: 'DrawerOpen', drawerCloseRoute: 'DrawerClose', drawerToggleRoute: 'DrawerToggle', ...commonProps, }; if (drawerWidth) { config.drawerWidth = drawerWidth; } if (drawerLockMode) { config.drawerLockMode = drawerLockMode; } return createDrawerNavigator(res, config); } if (overlay) { return createTabNavigatorHOC(OverlayRenderer)(res, { lazy, initialRouteName, contentComponent, initialRouteParams, order, ...commonProps, tabBarOptions: createTabBarOptions(commonProps), navigationOptions: createNavigationOptions(commonProps), }); } return createStackNavigator(res, { mode, initialRouteParams, initialRouteName, ...commonProps, transitionConfig, navigationOptions: createNavigationOptions(commonProps), }); }; dispatch = (action) => { if (this.externalDispatch) { this.externalAction = action; this.externalDispatch(action); } else if (this._navigator) { this._navigator.dispatch(action); } }; execute = (actionType, routeName, ...params) => { const res = uniteParams(routeName, params); const overridenType = res.type || actionType; const type = actionMap[overridenType] || overridenType; if (type === 'pop') { this[type](res); } else { this[type](routeName, res); } }; push = (routeName, data) => { const params = filterParam(data); this.dispatch({ type: StackActions.PUSH, routeName, params }); }; jump = (routeName, data) => { const params = filterParam(data); this.dispatch({ type: NavigationActions.NAVIGATE, routeName, params }); }; drawerOpen = () => { this.dispatch(DrawerActions.openDrawer()); }; drawerClose = () => { this.dispatch(DrawerActions.closeDrawer()); }; drawerToggle = () => { this.dispatch(DrawerActions.toggleDrawer()); }; refresh = (data, sceneKey = null) => { const params = filterParam(data); const { key } = getActiveState(this.state); this.dispatch( NavigationActions.setParams({ key: sceneKey || key, params, }), ); }; pop = ({ timeout, key, ...params } = {}) => { const res = filterParam(params); if (timeout) { setTimeout(() => this.pop(params), timeout); } else { this.dispatch(NavigationActions.back({ key })); if (res.refresh) { this.refresh(res.refresh); } } return true; }; popTo = (routeName, data) => { const params = filterParam(data); this.dispatch({ type: ActionConst.POP_TO, routeName, params }); }; popAndPush = (routeName, data) => { const params = filterParam(data); this.dispatch({ type: ActionConst.POP_AND_PUSH, routeName, params }); }; replace = (routeName, data) => { const params = filterParam(data); this.dispatch({ type: ActionConst.REPLACE, routeName, params }); }; reset = (routeName, data) => { const params = filterParam(data); const parent = getParent(this.state, routeName); this.dispatch( StackActions.reset({ index: 0, key: parent ? parent.key : null, actions: [ NavigationActions.navigate({ routeName, params, }), ], }), ); }; }