941 lines
28 KiB
JavaScript
941 lines
28 KiB
JavaScript
/**
|
|
* Copyright (c) 2015-present, Facebook, Inc.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @providesModule NavigatorIOS
|
|
* @flow
|
|
*/
|
|
'use strict';
|
|
|
|
const EventEmitter = require('EventEmitter');
|
|
const Image = require('Image');
|
|
const RCTNavigatorManager = require('NativeModules').NavigatorManager;
|
|
const React = require('React');
|
|
const PropTypes = require('prop-types');
|
|
const ReactNative = require('ReactNative');
|
|
const StaticContainer = require('StaticContainer.react');
|
|
const StyleSheet = require('StyleSheet');
|
|
const TVEventHandler = require('TVEventHandler');
|
|
const View = require('View');
|
|
const ViewPropTypes = require('ViewPropTypes');
|
|
|
|
const createReactClass = require('create-react-class');
|
|
const invariant = require('fbjs/lib/invariant');
|
|
const requireNativeComponent = require('requireNativeComponent');
|
|
|
|
/* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses an error
|
|
* found when Flow v0.54 was deployed. To see the error delete this comment and
|
|
* run Flow. */
|
|
const keyMirror = require('fbjs/lib/keyMirror');
|
|
|
|
const TRANSITIONER_REF = 'transitionerRef';
|
|
|
|
let __uid = 0;
|
|
function getuid() {
|
|
return __uid++;
|
|
}
|
|
|
|
class NavigatorTransitionerIOS extends React.Component<$FlowFixMeProps> {
|
|
requestSchedulingNavigation(cb) {
|
|
RCTNavigatorManager.requestSchedulingJavaScriptNavigation(
|
|
ReactNative.findNodeHandle(this),
|
|
cb
|
|
);
|
|
}
|
|
|
|
render() {
|
|
return (
|
|
<RCTNavigator {...this.props}/>
|
|
);
|
|
}
|
|
}
|
|
|
|
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<typeof SystemIconLabels>;
|
|
|
|
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<number>,
|
|
routeStack: Array<Route>,
|
|
requestedTopOfStack: number,
|
|
observedTopOfStack: number,
|
|
progress: number,
|
|
fromIndex: number,
|
|
toIndex: number,
|
|
makingNavigatorRequest: boolean,
|
|
updatingAllIndicesAtOrBeyond: ?number,
|
|
}
|
|
|
|
type Event = Object;
|
|
|
|
/**
|
|
* Think of `<NavigatorIOS>` 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.
|
|
* `<NavigatorIOS>` 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.
|
|
* `<NavigatorIOS>` 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
|
|
* [`React Navigation`](https://reactnavigation.org/) for a cross-platform
|
|
* solution in JavaScript, or check out either of these components for native
|
|
* solutions: [native-navigation](http://airbnb.io/native-navigation/),
|
|
* [react-native-navigation](https://github.com/wix/react-native-navigation).
|
|
*
|
|
* 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 PropTypes from 'prop-types';
|
|
* import React, { Component } from 'react';
|
|
* import { NavigatorIOS, Text } from 'react-native';
|
|
*
|
|
* export default class NavigatorIOSApp extends Component {
|
|
* render() {
|
|
* return (
|
|
* <NavigatorIOS
|
|
* initialRoute={{
|
|
* component: MyScene,
|
|
* title: 'My Initial Scene',
|
|
* }}
|
|
* style={{flex: 1}}
|
|
* />
|
|
* );
|
|
* }
|
|
* }
|
|
*
|
|
* class MyScene extends Component {
|
|
* static propTypes = {
|
|
* title: PropTypes.string.isRequired,
|
|
* navigator: PropTypes.object.isRequired,
|
|
* }
|
|
*
|
|
* _onForward = () => {
|
|
* this.props.navigator.push({
|
|
* title: 'Scene ' + nextIndex,
|
|
* });
|
|
* }
|
|
*
|
|
* render() {
|
|
* return (
|
|
* <View>
|
|
* <Text>Current Scene: { this.props.title }</Text>
|
|
* <TouchableHighlight onPress={this._onForward}>
|
|
* <Text>Tap me to load the next scene</Text>
|
|
* </TouchableHighlight>
|
|
* </View>
|
|
* )
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* 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(
|
|
* <TouchableHighlight onPress={() => this._handleNextPress(nextRoute)}>
|
|
* <Text style={{marginTop: 200, alignSelf: 'center'}}>
|
|
* See you on the other nav {this.props.myProp}!
|
|
* </Text>
|
|
* </TouchableHighlight>
|
|
* );
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* 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 (
|
|
* <NavigatorIOS
|
|
* ref='nav'
|
|
* initialRoute={{
|
|
* component: MyView,
|
|
* title: 'Foo This',
|
|
* passProps: { myProp: 'foo' },
|
|
* rightButtonTitle: 'Add',
|
|
* onRightButtonPress: () => 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 (
|
|
* <NavigatorIOS
|
|
* //...
|
|
* style={{flex: 1}}
|
|
* barTintColor='#ffffcc'
|
|
* />
|
|
* );
|
|
* }
|
|
* ```
|
|
*
|
|
* In the example above the navigation bar color is changed when the new route
|
|
* is pushed.
|
|
*
|
|
*/
|
|
const NavigatorIOS = createReactClass({
|
|
displayName: 'NavigatorIOS',
|
|
|
|
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 component.
|
|
*/
|
|
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: ViewPropTypes.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 style of the navigation bar. Supported values are 'default', 'black'.
|
|
* Use 'black' instead of setting `barTintColor` to black. This produces
|
|
* a navigation bar with the native iOS style with higher translucency.
|
|
*/
|
|
barStyle: PropTypes.oneOf(['default', 'black']),
|
|
|
|
/**
|
|
* The text color of the navigation bar title.
|
|
*/
|
|
titleTextColor: PropTypes.string,
|
|
|
|
/**
|
|
* Boolean 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: ViewPropTypes.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 style of the navigation bar. Supported values are 'default', 'black'.
|
|
* Use 'black' instead of setting `barTintColor` to black. This produces
|
|
* a navigation bar with the native iOS style with higher translucency.
|
|
*/
|
|
barStyle: PropTypes.oneOf(['default', 'black']),
|
|
|
|
/**
|
|
* 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),
|
|
|
|
UNSAFE_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,
|
|
};
|
|
},
|
|
|
|
componentDidMount: function() {
|
|
this._enableTVEventHandler();
|
|
},
|
|
|
|
componentWillUnmount: function() {
|
|
this._disableTVEventHandler();
|
|
},
|
|
|
|
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.
|
|
let 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: PropTypes.func,
|
|
focusEmitter: PropTypes.instanceOf(EventEmitter),
|
|
},
|
|
|
|
_tryLockNavigator: function(cb: () => void) {
|
|
this.refs[TRANSITIONER_REF].requestSchedulingNavigation(
|
|
(acquiredLock) => acquiredLock && cb()
|
|
);
|
|
},
|
|
|
|
_handleNavigatorStackChanged: function(e: Event) {
|
|
const newObservedTopOfStack = e.nativeEvent.stackLength - 1;
|
|
|
|
invariant(
|
|
newObservedTopOfStack <= this.state.requestedTopOfStack,
|
|
'No navigator item should be pushed without JS knowing about it %s %s', newObservedTopOfStack, this.state.requestedTopOfStack
|
|
);
|
|
const 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.
|
|
const 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).
|
|
const 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,
|
|
});
|
|
},
|
|
|
|
/**
|
|
* 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(() => {
|
|
|
|
const nextStack = this.state.routeStack.concat([route]);
|
|
const 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(() => {
|
|
const newRequestedTopOfStack = this.state.requestedTopOfStack - n;
|
|
invariant(newRequestedTopOfStack >= 0, 'Cannot pop below 0');
|
|
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
|
|
const nextIDStack = this.state.idStack.slice();
|
|
const nextRouteStack = this.state.routeStack.slice();
|
|
nextIDStack[index] = getuid();
|
|
nextRouteStack[index] = route;
|
|
|
|
this.setState({
|
|
idStack: nextIDStack,
|
|
routeStack: nextRouteStack,
|
|
makingNavigatorRequest: false,
|
|
updatingAllIndicesAtOrBeyond: index,
|
|
});
|
|
|
|
},
|
|
|
|
/**
|
|
* 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) {
|
|
const indexOfRoute = this.state.routeStack.indexOf(route);
|
|
invariant(
|
|
indexOfRoute !== -1,
|
|
'Calling pop to route for a route that doesn\'t exist!'
|
|
);
|
|
const 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) {
|
|
const {component, wrapperStyle, passProps, ...route} = routeArg;
|
|
const {itemWrapperStyle, ...props} = this.props;
|
|
const shouldUpdateChild =
|
|
this.state.updatingAllIndicesAtOrBeyond != null &&
|
|
this.state.updatingAllIndicesAtOrBeyond >= i;
|
|
const Component = component;
|
|
return (
|
|
<StaticContainer key={'nav' + i} shouldUpdate={shouldUpdateChild}>
|
|
<RCTNavigatorItem
|
|
{...props}
|
|
{...route}
|
|
style={[
|
|
styles.stackItem,
|
|
itemWrapperStyle,
|
|
wrapperStyle
|
|
]}>
|
|
<Component
|
|
navigator={this.navigator}
|
|
route={route}
|
|
{...passProps}
|
|
/>
|
|
</RCTNavigatorItem>
|
|
</StaticContainer>
|
|
);
|
|
},
|
|
|
|
_renderNavigationStackItems: function() {
|
|
const shouldRecurseToNavigator =
|
|
this.state.makingNavigatorRequest ||
|
|
this.state.updatingAllIndicesAtOrBeyond !== null;
|
|
// If not recursing update to navigator at all, may as well avoid
|
|
// computation of navigator children.
|
|
const items = shouldRecurseToNavigator ?
|
|
this.state.routeStack.map(this._routeToStackItem) : null;
|
|
return (
|
|
<StaticContainer shouldUpdate={shouldRecurseToNavigator}>
|
|
<NavigatorTransitionerIOS
|
|
ref={TRANSITIONER_REF}
|
|
style={styles.transitioner}
|
|
// $FlowFixMe(>=0.41.0)
|
|
vertical={this.props.vertical}
|
|
requestedTopOfStack={this.state.requestedTopOfStack}
|
|
onNavigationComplete={this._handleNavigationComplete}
|
|
interactivePopGestureEnabled={this.props.interactivePopGestureEnabled}>
|
|
{items}
|
|
</NavigatorTransitionerIOS>
|
|
</StaticContainer>
|
|
);
|
|
},
|
|
|
|
_tvEventHandler: (undefined: ?TVEventHandler),
|
|
|
|
_enableTVEventHandler: function() {
|
|
this._tvEventHandler = new TVEventHandler();
|
|
this._tvEventHandler.enable(this, function(cmp, evt) {
|
|
if (evt && evt.eventType === 'menu') {
|
|
cmp.pop();
|
|
}
|
|
});
|
|
},
|
|
|
|
_disableTVEventHandler: function() {
|
|
if (this._tvEventHandler) {
|
|
this._tvEventHandler.disable();
|
|
delete this._tvEventHandler;
|
|
}
|
|
},
|
|
|
|
render: function() {
|
|
return (
|
|
// $FlowFixMe(>=0.41.0)
|
|
<View style={this.props.style}>
|
|
{this._renderNavigationStackItems()}
|
|
</View>
|
|
);
|
|
},
|
|
});
|
|
|
|
const styles = StyleSheet.create({
|
|
stackItem: {
|
|
backgroundColor: 'white',
|
|
overflow: 'hidden',
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
},
|
|
transitioner: {
|
|
flex: 1,
|
|
},
|
|
});
|
|
|
|
const RCTNavigator = requireNativeComponent('RCTNavigator');
|
|
const RCTNavigatorItem = requireNativeComponent('RCTNavItem');
|
|
|
|
module.exports = NavigatorIOS;
|