/** * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule NavigatorIOS * @flow */ 'use strict'; var EventEmitter = require('EventEmitter'); var Image = require('Image'); var NavigationContext = require('NavigationContext'); var RCTNavigatorManager = require('NativeModules').NavigatorManager; var React = require('React'); var ReactNative = require('ReactNative'); var StaticContainer = require('StaticContainer.react'); var StyleSheet = require('StyleSheet'); var View = require('View'); var invariant = require('fbjs/lib/invariant'); var logError = require('logError'); var requireNativeComponent = require('requireNativeComponent'); const keyMirror = require('fbjs/lib/keyMirror'); var TRANSITIONER_REF = 'transitionerRef'; var PropTypes = React.PropTypes; var __uid = 0; function getuid() { return __uid++; } class NavigatorTransitionerIOS extends React.Component { requestSchedulingNavigation = (cb) => { RCTNavigatorManager.requestSchedulingJavaScriptNavigation( ReactNative.findNodeHandle(this), logError, cb ); }; render() { return ( ); } } const SystemIconLabels = { done: true, cancel: true, edit: true, save: true, add: true, compose: true, reply: true, action: true, organize: true, bookmarks: true, search: true, refresh: true, stop: true, camera: true, trash: true, play: true, pause: true, rewind: true, 'fast-forward': true, undo: true, redo: true, 'page-curl': true, }; const SystemIcons = keyMirror(SystemIconLabels); type SystemButtonType = $Enum; type Route = { component: Function, title: string, titleImage?: Object, passProps?: Object, backButtonTitle?: string, backButtonIcon?: Object, leftButtonTitle?: string, leftButtonIcon?: Object, leftButtonSystemIcon?: SystemButtonType; onLeftButtonPress?: Function, rightButtonTitle?: string, rightButtonIcon?: Object, rightButtonSystemIcon?: SystemButtonType; onRightButtonPress?: Function, wrapperStyle?: any, }; type State = { idStack: Array, routeStack: Array, requestedTopOfStack: number, observedTopOfStack: number, progress: number, fromIndex: number, toIndex: number, makingNavigatorRequest: boolean, updatingAllIndicesAtOrBeyond: ?number, } type Event = Object; /** * Think of `` as simply a component that renders an * `RCTNavigator`, and moves the `RCTNavigator`'s `requestedTopOfStack` pointer * forward and backward. The `RCTNavigator` interprets changes in * `requestedTopOfStack` to be pushes and pops of children that are rendered. * `` always ensures that whenever the `requestedTopOfStack` * pointer is moved, that we've also rendered enough children so that the * `RCTNavigator` can carry out the push/pop with those children. * `` also removes children that will no longer be needed * (after the pop of a child has been fully completed/animated out). */ /** * `NavigatorIOS` is a wrapper around * [`UINavigationController`](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UINavigationController_Class/), * enabling you to implement a navigation stack. It works exactly the same as it * would on a native app using `UINavigationController`, providing the same * animations and behavior from UIKIt. * * As the name implies, it is only available on iOS. Take a look at * [`Navigator`](/react-native/docs/navigator.html) for a similar solution for your * cross-platform needs, or check out * [react-native-navigation](https://github.com/wix/react-native-navigation), a * component that aims to provide native navigation on both iOS and Android. * * To set up the navigator, provide the `initialRoute` prop with a route * object. A route object is used to describe each scene that your app * navigates to. `initialRoute` represents the first route in your navigator. * * ``` * import React, { Component, PropTypes } from 'react'; * import { NavigatorIOS, Text } from 'react-native'; * * export default class NavigatorIOSApp extends Component { * render() { * return ( * * ); * } * } * * class MyScene extends Component { * static propTypes = { * title: PropTypes.string.isRequired, * navigator: PropTypes.object.isRequired, * } * * _onForward = () => { * this.props.navigator.push({ * title: 'Scene ' + nextIndex, * }); * } * * render() { * return ( * * Current Scene: { this.props.title } * * Tap me to load the next scene * * * ) * } * } * ``` * * In this code, the navigator renders the component specified in initialRoute, * which in this case is `MyScene`. This component will receive a `route` prop * and a `navigator` prop representing the navigator. The navigator's navigation * bar will render the title for the current scene, "My Initial Scene". * * You can optionally pass in a `passProps` property to your `initialRoute`. * `NavigatorIOS` passes this in as props to the rendered component: * * ``` * initialRoute={{ * component: MyScene, * title: 'My Initial Scene', * passProps: { myProp: 'foo' } * }} * ``` * * You can then access the props passed in via `{this.props.myProp}`. * * #### Handling Navigation * * To trigger navigation functionality such as pushing or popping a view, you * have access to a `navigator` object. The object is passed in as a prop to any * component that is rendered by `NavigatorIOS`. You can then call the * relevant methods to perform the navigation action you need: * * ``` * class MyView extends Component { * _handleBackPress() { * this.props.navigator.pop(); * } * * _handleNextPress(nextRoute) { * this.props.navigator.push(nextRoute); * } * * render() { * const nextRoute = { * component: MyView, * title: 'Bar That', * passProps: { myProp: 'bar' } * }; * return( * this._handleNextPress(nextRoute)}> * * See you on the other nav {this.props.myProp}! * * * ); * } * } * ``` * * You can also trigger navigator functionality from the `NavigatorIOS` * component: * * ``` * class NavvyIOS extends Component { * _handleNavigationRequest() { * this.refs.nav.push({ * component: MyView, * title: 'Genius', * passProps: { myProp: 'genius' }, * }); * } * * render() { * return ( * this._handleNavigationRequest(), * }} * style={{flex: 1}} * /> * ); * } * } * ``` * * The code above adds a `_handleNavigationRequest` private method that is * invoked from the `NavigatorIOS` component when the right navigation bar item * is pressed. To get access to the navigator functionality, a reference to it * is saved in the `ref` prop and later referenced to push a new scene into the * navigation stack. * * #### Navigation Bar Configuration * * Props passed to `NavigatorIOS` will set the default configuration * for the navigation bar. Props passed as properties to a route object will set * the configuration for that route's navigation bar, overriding any props * passed to the `NavigatorIOS` component. * * ``` * _handleNavigationRequest() { * this.refs.nav.push({ * //... * passProps: { myProp: 'genius' }, * barTintColor: '#996699', * }); * } * * render() { * return ( * * ); * } * ``` * * In the example above the navigation bar color is changed when the new route * is pushed. * */ var NavigatorIOS = React.createClass({ propTypes: { /** * NavigatorIOS uses `route` objects to identify child views, their props, * and navigation bar configuration. Navigation operations such as push * operations expect routes to look like this the `initialRoute`. */ initialRoute: PropTypes.shape({ /** * The React Class to render for this route */ component: PropTypes.func.isRequired, /** * The title displayed in the navigation bar and the back button for this * route. */ title: PropTypes.string.isRequired, /** * If set, a title image will appear instead of the text title. */ titleImage: Image.propTypes.source, /** * Use this to specify additional props to pass to the rendered * component. `NavigatorIOS` will automatically pass in `route` and * `navigator` props to the comoponent. */ passProps: PropTypes.object, /** * If set, the left navigation button image will be displayed using this * source. Note that this doesn't apply to the header of the current * view, but to those views that are subsequently pushed. */ backButtonIcon: Image.propTypes.source, /** * If set, the left navigation button text will be set to this. Note that * this doesn't apply to the left button of the current view, but to * those views that are subsequently pushed */ backButtonTitle: PropTypes.string, /** * If set, the left navigation button image will be displayed using * this source. */ leftButtonIcon: Image.propTypes.source, /** * If set, the left navigation button will display this text. */ leftButtonTitle: PropTypes.string, /** * If set, the left header button will appear with this system icon * * Supported icons are `done`, `cancel`, `edit`, `save`, `add`, * `compose`, `reply`, `action`, `organize`, `bookmarks`, `search`, * `refresh`, `stop`, `camera`, `trash`, `play`, `pause`, `rewind`, * `fast-forward`, `undo`, `redo`, and `page-curl` */ leftButtonSystemIcon: PropTypes.oneOf(Object.keys(SystemIcons)), /** * This function will be invoked when the left navigation bar item is * pressed. */ onLeftButtonPress: PropTypes.func, /** * If set, the right navigation button image will be displayed using * this source. */ rightButtonIcon: Image.propTypes.source, /** * If set, the right navigation button will display this text. */ rightButtonTitle: PropTypes.string, /** * If set, the right header button will appear with this system icon * * See leftButtonSystemIcon for supported icons */ rightButtonSystemIcon: PropTypes.oneOf(Object.keys(SystemIcons)), /** * This function will be invoked when the right navigation bar item is * pressed. */ onRightButtonPress: PropTypes.func, /** * Styles for the navigation item containing the component. */ wrapperStyle: View.propTypes.style, /** * Boolean value that indicates whether the navigation bar is hidden. */ navigationBarHidden: PropTypes.bool, /** * Boolean value that indicates whether to hide the 1px hairline * shadow. */ shadowHidden: PropTypes.bool, /** * The color used for the buttons in the navigation bar. */ tintColor: PropTypes.string, /** * The background color of the navigation bar. */ barTintColor: PropTypes.string, /** * The text color of the navigation bar title. */ titleTextColor: PropTypes.string, /** * Bboolean value that indicates whether the navigation bar is * translucent. */ translucent: PropTypes.bool, }).isRequired, /** * Boolean value that indicates whether the navigation bar is hidden * by default. */ navigationBarHidden: PropTypes.bool, /** * Boolean value that indicates whether to hide the 1px hairline shadow * by default. */ shadowHidden: PropTypes.bool, /** * The default wrapper style for components in the navigator. * A common use case is to set the `backgroundColor` for every scene. */ itemWrapperStyle: View.propTypes.style, /** * The default color used for the buttons in the navigation bar. */ tintColor: PropTypes.string, /** * The default background color of the navigation bar. */ barTintColor: PropTypes.string, /** * The default text color of the navigation bar title. */ titleTextColor: PropTypes.string, /** * Boolean value that indicates whether the navigation bar is * translucent by default */ translucent: PropTypes.bool, /** * Boolean value that indicates whether the interactive pop gesture is * enabled. This is useful for enabling/disabling the back swipe navigation * gesture. * * If this prop is not provided, the default behavior is for the back swipe * gesture to be enabled when the navigation bar is shown and disabled when * the navigation bar is hidden. Once you've provided the * `interactivePopGestureEnabled` prop, you can never restore the default * behavior. */ interactivePopGestureEnabled: PropTypes.bool, }, navigator: (undefined: ?Object), navigationContext: new NavigationContext(), componentWillMount: function() { // Precompute a pack of callbacks that's frequently generated and passed to // instances. this.navigator = { push: this.push, pop: this.pop, popN: this.popN, replace: this.replace, replaceAtIndex: this.replaceAtIndex, replacePrevious: this.replacePrevious, replacePreviousAndPop: this.replacePreviousAndPop, resetTo: this.resetTo, popToRoute: this.popToRoute, popToTop: this.popToTop, navigationContext: this.navigationContext, }; this._emitWillFocus(this.state.routeStack[this.state.observedTopOfStack]); }, componentDidMount: function() { this._emitDidFocus(this.state.routeStack[this.state.observedTopOfStack]); }, componentWillUnmount: function() { this.navigationContext.dispose(); this.navigationContext = new NavigationContext(); }, getDefaultProps: function(): Object { return { translucent: true, }; }, getInitialState: function(): State { return { idStack: [getuid()], routeStack: [this.props.initialRoute], // The navigation index that we wish to push/pop to. requestedTopOfStack: 0, // The last index that native has sent confirmation of completed push/pop // for. At this point, we can discard any views that are beyond the // `requestedTopOfStack`. A value of `null` means we have not received // any confirmation, ever. We may receive an `observedTopOfStack` without // ever requesting it - native can instigate pops of its own with the // backswipe gesture. observedTopOfStack: 0, progress: 1, fromIndex: 0, toIndex: 0, // Whether or not we are making a navigator request to push/pop. (Used // for performance optimization). makingNavigatorRequest: false, // Whether or not we are updating children of navigator and if so (not // `null`) which index marks the beginning of all updates. Used for // performance optimization. updatingAllIndicesAtOrBeyond: 0, }; }, _toFocusOnNavigationComplete: (undefined: any), _handleFocusRequest: function(item: any) { if (this.state.makingNavigatorRequest) { this._toFocusOnNavigationComplete = item; } else { this._getFocusEmitter().emit('focus', item); } }, _focusEmitter: (undefined: ?EventEmitter), _getFocusEmitter: function(): EventEmitter { // Flow not yet tracking assignments to instance fields. var focusEmitter = this._focusEmitter; if (!focusEmitter) { focusEmitter = new EventEmitter(); this._focusEmitter = focusEmitter; } return focusEmitter; }, getChildContext: function(): { onFocusRequested: Function, focusEmitter: EventEmitter, } { return { onFocusRequested: this._handleFocusRequest, focusEmitter: this._getFocusEmitter(), }; }, childContextTypes: { onFocusRequested: React.PropTypes.func, focusEmitter: React.PropTypes.instanceOf(EventEmitter), }, _tryLockNavigator: function(cb: () => void) { this.refs[TRANSITIONER_REF].requestSchedulingNavigation( (acquiredLock) => acquiredLock && cb() ); }, _handleNavigatorStackChanged: function(e: Event) { var newObservedTopOfStack = e.nativeEvent.stackLength - 1; this._emitDidFocus(this.state.routeStack[newObservedTopOfStack]); invariant( newObservedTopOfStack <= this.state.requestedTopOfStack, 'No navigator item should be pushed without JS knowing about it %s %s', newObservedTopOfStack, this.state.requestedTopOfStack ); var wasWaitingForConfirmation = this.state.requestedTopOfStack !== this.state.observedTopOfStack; if (wasWaitingForConfirmation) { invariant( newObservedTopOfStack === this.state.requestedTopOfStack, 'If waiting for observedTopOfStack to reach requestedTopOfStack, ' + 'the only valid observedTopOfStack should be requestedTopOfStack.' ); } // Mark the most recent observation regardless of if we can lock the // navigator. `observedTopOfStack` merely represents what we've observed // and this first `setState` is only executed to update debugging // overlays/navigation bar. // Also reset progress, toIndex, and fromIndex as they might not end // in the correct states for a two possible reasons: // Progress isn't always 0 or 1 at the end, the system rounds // If the Navigator is offscreen these values won't be updated // TOOD: Revisit this decision when no longer relying on native navigator. var nextState = { observedTopOfStack: newObservedTopOfStack, makingNavigatorRequest: false, updatingAllIndicesAtOrBeyond: null, progress: 1, toIndex: newObservedTopOfStack, fromIndex: newObservedTopOfStack, }; this.setState(nextState, this._eliminateUnneededChildren); }, _eliminateUnneededChildren: function() { // Updating the indices that we're deleting and that's all. (Truth: Nothing // even uses the indices in this case, but let's make this describe the // truth anyways). var updatingAllIndicesAtOrBeyond = this.state.routeStack.length > this.state.observedTopOfStack + 1 ? this.state.observedTopOfStack + 1 : null; this.setState({ idStack: this.state.idStack.slice(0, this.state.observedTopOfStack + 1), routeStack: this.state.routeStack.slice(0, this.state.observedTopOfStack + 1), // Now we rerequest the top of stack that we observed. requestedTopOfStack: this.state.observedTopOfStack, makingNavigatorRequest: true, updatingAllIndicesAtOrBeyond: updatingAllIndicesAtOrBeyond, }); }, _emitDidFocus: function(route: Route) { this.navigationContext.emit('didfocus', {route: route}); }, _emitWillFocus: function(route: Route) { this.navigationContext.emit('willfocus', {route: route}); }, /** * Navigate forward to a new route. * @param route The new route to navigate to. */ push: function(route: Route) { invariant(!!route, 'Must supply route to push'); // Make sure all previous requests are caught up first. Otherwise reject. if (this.state.requestedTopOfStack === this.state.observedTopOfStack) { this._tryLockNavigator(() => { this._emitWillFocus(route); var nextStack = this.state.routeStack.concat([route]); var nextIDStack = this.state.idStack.concat([getuid()]); this.setState({ // We have to make sure that we've also supplied enough views to // satisfy our request to adjust the `requestedTopOfStack`. idStack: nextIDStack, routeStack: nextStack, requestedTopOfStack: nextStack.length - 1, makingNavigatorRequest: true, updatingAllIndicesAtOrBeyond: nextStack.length - 1, }); }); } }, /** * Go back N scenes at once. When N=1, behavior matches `pop()`. * @param n The number of scenes to pop. */ popN: function(n: number) { if (n === 0) { return; } // Make sure all previous requests are caught up first. Otherwise reject. if (this.state.requestedTopOfStack === this.state.observedTopOfStack) { if (this.state.requestedTopOfStack > 0) { this._tryLockNavigator(() => { var newRequestedTopOfStack = this.state.requestedTopOfStack - n; invariant(newRequestedTopOfStack >= 0, 'Cannot pop below 0'); this._emitWillFocus(this.state.routeStack[newRequestedTopOfStack]); this.setState({ requestedTopOfStack: newRequestedTopOfStack, makingNavigatorRequest: true, updatingAllIndicesAtOrBeyond: this.state.requestedTopOfStack - n, }); }); } } }, /** * Pop back to the previous scene. */ pop: function() { this.popN(1); }, /** * Replace a route in the navigation stack. * * @param route The new route that will replace the specified one. * @param index The route into the stack that should be replaced. * If it is negative, it counts from the back of the stack. */ replaceAtIndex: function(route: Route, index: number) { invariant(!!route, 'Must supply route to replace'); if (index < 0) { index += this.state.routeStack.length; } if (this.state.routeStack.length <= index) { return; } // I don't believe we need to lock for a replace since there's no // navigation actually happening var nextIDStack = this.state.idStack.slice(); var nextRouteStack = this.state.routeStack.slice(); nextIDStack[index] = getuid(); nextRouteStack[index] = route; this.setState({ idStack: nextIDStack, routeStack: nextRouteStack, makingNavigatorRequest: false, updatingAllIndicesAtOrBeyond: index, }); this._emitWillFocus(route); this._emitDidFocus(route); }, /** * Replace the route for the current scene and immediately * load the view for the new route. * @param route The new route to navigate to. */ replace: function(route: Route) { this.replaceAtIndex(route, -1); }, /** * Replace the route/view for the previous scene. * @param route The new route to will replace the previous scene. */ replacePrevious: function(route: Route) { this.replaceAtIndex(route, -2); }, /** * Go back to the topmost item in the navigation stack. */ popToTop: function() { this.popToRoute(this.state.routeStack[0]); }, /** * Go back to the item for a particular route object. * @param route The new route to navigate to. */ popToRoute: function(route: Route) { var indexOfRoute = this.state.routeStack.indexOf(route); invariant( indexOfRoute !== -1, 'Calling pop to route for a route that doesn\'t exist!' ); var numToPop = this.state.routeStack.length - indexOfRoute - 1; this.popN(numToPop); }, /** * Replaces the previous route/view and transitions back to it. * @param route The new route that replaces the previous scene. */ replacePreviousAndPop: function(route: Route) { // Make sure all previous requests are caught up first. Otherwise reject. if (this.state.requestedTopOfStack !== this.state.observedTopOfStack) { return; } if (this.state.routeStack.length < 2) { return; } this._tryLockNavigator(() => { this.replacePrevious(route); this.setState({ requestedTopOfStack: this.state.requestedTopOfStack - 1, makingNavigatorRequest: true, }); }); }, /** * Replaces the top item and pop to it. * @param route The new route that will replace the topmost item. */ resetTo: function(route: Route) { invariant(!!route, 'Must supply route to push'); // Make sure all previous requests are caught up first. Otherwise reject. if (this.state.requestedTopOfStack !== this.state.observedTopOfStack) { return; } this.replaceAtIndex(route, 0); this.popToRoute(route); }, _handleNavigationComplete: function(e: Event) { // Don't propagate to other NavigatorIOS instances this is nested in: e.stopPropagation(); if (this._toFocusOnNavigationComplete) { this._getFocusEmitter().emit('focus', this._toFocusOnNavigationComplete); this._toFocusOnNavigationComplete = null; } this._handleNavigatorStackChanged(e); }, _routeToStackItem: function(routeArg: Route, i: number) { var {component, wrapperStyle, passProps, ...route} = routeArg; var {itemWrapperStyle, ...props} = this.props; var shouldUpdateChild = this.state.updatingAllIndicesAtOrBeyond != null && this.state.updatingAllIndicesAtOrBeyond >= i; var Component = component; return ( ); }, _renderNavigationStackItems: function() { var shouldRecurseToNavigator = this.state.makingNavigatorRequest || this.state.updatingAllIndicesAtOrBeyond !== null; // If not recursing update to navigator at all, may as well avoid // computation of navigator children. var items = shouldRecurseToNavigator ? this.state.routeStack.map(this._routeToStackItem) : null; return ( {items} ); }, render: function() { return ( {this._renderNavigationStackItems()} ); }, }); var styles = StyleSheet.create({ stackItem: { backgroundColor: 'white', overflow: 'hidden', position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, }, transitioner: { flex: 1, }, }); var RCTNavigator = requireNativeComponent('RCTNavigator'); var RCTNavigatorItem = requireNativeComponent('RCTNavItem'); module.exports = NavigatorIOS;