reactxp-navigation
Version:
Plugin for ReactXP that provides a navigation framework
506 lines (505 loc) • 21.4 kB
JavaScript
"use strict";
/*
* Navigator.tsx
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT license.
*
* Web specific implementation of Navigator. This is inspired from React.Navigator.
* This component is set with props, which are callback methods. It is primarily driven
* by state updates instigated by its public helpers like immediatelyResetRouteStack, push,
* pop, which update the state and cause transitions.
*/
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
var React = require("react");
var ReactDOM = require("react-dom");
var rebound = require("rebound");
var reactxp_1 = require("reactxp");
var _ = require("../common/lodashMini");
var Types = require("../common/Types");
exports.Types = Types;
var NavigatorSceneConfigFactory_1 = require("./NavigatorSceneConfigFactory");
var Types_1 = require("../common/Types");
// Default styles
var _styles = {
container: reactxp_1.Styles.createViewStyle({
flex: 1,
flexDirection: 'column',
alignSelf: 'stretch',
overflow: 'hidden'
}),
defaultSceneStyle: reactxp_1.Styles.createViewStyle({
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
top: 0
}),
baseScene: reactxp_1.Styles.createViewStyle({
position: 'absolute',
overflow: 'hidden',
left: 0,
right: 0,
bottom: 0,
top: 0
}),
disabledScene: reactxp_1.Styles.createViewStyle({
top: 0,
bottom: 0,
flex: 1
}),
transitioner: reactxp_1.Styles.createViewStyle({
flex: 1,
flexDirection: 'column',
backgroundColor: 'transparent',
overflow: 'hidden',
alignItems: 'stretch'
}),
sceneStyle: reactxp_1.Styles.createViewStyle({
flex: 1,
shadowOffset: { height: 0, width: 0 },
shadowRadius: 40,
shadowColor: 'rgba(0, 0, 0, 0.2)'
})
};
var NavigatorImpl = /** @class */ (function (_super) {
__extends(NavigatorImpl, _super);
// Receives initial props and sets initial state for Navigator
function NavigatorImpl(initialProps) {
var _this = _super.call(this, initialProps) || this;
// Keep a map of all rendered scenes, keyed off their routeId
_this._renderedSceneMap = {};
_this._sceneRefs = {};
_this._onMountContainer = function (comp) {
_this._containerRef = comp;
};
// Default navigator state
_this.state = {
sceneConfigStack: [],
routeStack: [],
presentedIndex: 0,
transitionFromIndex: undefined,
transitionQueue: []
};
return _this;
}
NavigatorImpl.prototype.UNSAFE_componentWillMount = function () {
var _this = this;
this.springSystem = new rebound.SpringSystem();
this.spring = this.springSystem.createSpring();
this.spring.setRestSpeedThreshold(0.05);
this.spring.setCurrentValue(0).setAtRest();
this.spring.addListener({
onSpringUpdate: function () {
_this._handleSpringUpdate();
},
onSpringAtRest: function () {
_this._completeTransition();
}
});
};
NavigatorImpl.prototype.componentDidMount = function () {
this._updateDimensionsCache();
this._handleSpringUpdate();
};
NavigatorImpl.prototype.componentDidUpdate = function () {
this._updateDimensionsCache();
};
NavigatorImpl.prototype.render = function () {
var _this = this;
var newRenderedSceneMap = {};
var scenes;
if (this.state.routeStack.length > 0) {
scenes = this.state.routeStack.map(function (route, index) {
var renderedScene;
if (_this._renderedSceneMap[route.routeId] &&
index !== _this.state.presentedIndex) {
renderedScene = _this._renderedSceneMap[route.routeId];
}
else {
renderedScene = _this._renderNavigatorScene(route, index);
}
newRenderedSceneMap[route.routeId] = renderedScene;
return renderedScene;
});
}
else {
scenes = [];
}
this._renderedSceneMap = _.clone(newRenderedSceneMap);
return (React.createElement(reactxp_1.View, { key: 'container', style: _styles.container },
React.createElement(reactxp_1.View, { style: _styles.transitioner, ref: this._onMountContainer }, scenes)));
};
// Public Navigator Helper methods. These methods modify Navigator state, which kicks of
// re-renders for the Navigator
NavigatorImpl.prototype.jumpTo = function (route) {
var destIndex = this.state.routeStack.indexOf(route);
this._jumpN(destIndex - this.state.presentedIndex);
};
NavigatorImpl.prototype.jumpForward = function () {
this._jumpN(1);
};
NavigatorImpl.prototype.jumpBack = function () {
this._jumpN(-1);
};
NavigatorImpl.prototype.push = function (route) {
var _this = this;
this._invariant(!!route, 'Must supply route to push');
var activeLength = this.state.presentedIndex + 1;
var activeStack = this.state.routeStack.slice(0, activeLength);
var activeAnimationConfigStack = this.state.sceneConfigStack.slice(0, activeLength);
var nextStack = activeStack.concat([route]);
var destIndex = nextStack.length - 1;
var nextAnimationConfigStack = activeAnimationConfigStack.concat([
this._getSceneConfigFromRoute(route)
]);
this.setState({
routeStack: nextStack,
sceneConfigStack: nextAnimationConfigStack
}, function () {
_this._enableScene(destIndex);
_this._transitionTo(destIndex);
});
};
NavigatorImpl.prototype.immediatelyResetRouteStack = function (nextRouteStack) {
var _this = this;
var destIndex = nextRouteStack.length - 1;
this.setState({
// Build a sceneConfigStack
sceneConfigStack: _.map(nextRouteStack, function (route) { return _this._getSceneConfigFromRoute(route); }),
routeStack: nextRouteStack,
presentedIndex: destIndex,
transitionFromIndex: undefined,
transitionQueue: []
}, function () {
_this._handleSpringUpdate();
if (destIndex >= 0) {
_this._enableScene(destIndex, true);
}
});
};
NavigatorImpl.prototype.pop = function () {
if (this.state.transitionQueue.length) {
// This is the workaround to prevent user from firing multiple `pop()` calls that may pop the routes beyond
// the limit. Because `this.state.presentedIndex` does not update until the transition starts, we can't
// reliably use `this.state.presentedIndex` to know whether we can safely keep popping the routes or not at
// this moment.
return;
}
if (this.state.presentedIndex > 0) {
this._popN(1);
}
};
// This method replaces the route at the given index of the stack and pops to that index.
NavigatorImpl.prototype.replaceAtIndex = function (route, index) {
this._invariant(!!route, 'Must supply route to replace');
if (index < 0) {
index += this.state.routeStack.length;
}
if (this.state.routeStack.length <= index) {
return;
}
var nextRouteStack = this.state.routeStack.slice(0, index + 1);
var nextAnimationModeStack = this.state.sceneConfigStack.slice(0, index + 1);
nextRouteStack[index] = route;
nextAnimationModeStack[index] = this._getSceneConfigFromRoute(route);
this.setState({
routeStack: nextRouteStack,
sceneConfigStack: nextAnimationModeStack
});
};
NavigatorImpl.prototype.replace = function (route) {
this.replaceAtIndex(route, this.state.presentedIndex);
};
NavigatorImpl.prototype.replacePrevious = function (route) {
this.replaceAtIndex(route, this.state.presentedIndex - 1);
};
NavigatorImpl.prototype.popToTop = function () {
this.popToRoute(this.state.routeStack[0]);
};
NavigatorImpl.prototype.popToRoute = function (route) {
var indexOfRoute = this.state.routeStack.indexOf(route);
this._invariant(indexOfRoute !== -1, 'Calling popToRoute for a route that doesn\'t exist!');
var numToPop = this.state.presentedIndex - indexOfRoute;
this._popN(numToPop);
};
NavigatorImpl.prototype.replacePreviousAndPop = function (route) {
if (this.state.routeStack.length < 2) {
return;
}
this.replacePrevious(route);
this.pop();
};
NavigatorImpl.prototype.getCurrentRoutes = function () {
// Clone before returning to avoid caller mutating the stack.
return this.state.routeStack.slice();
};
NavigatorImpl.prototype._updateDimensionsCache = function () {
if (this._containerRef) {
var transitioner = ReactDOM.findDOMNode(this._containerRef);
if (transitioner) {
this._dimensions = {
width: transitioner.offsetWidth,
height: transitioner.offsetHeight
};
}
}
};
// Helper method to extract Navigator's Scene config from the route
NavigatorImpl.prototype._getSceneConfigFromRoute = function (route) {
// route exists? query the factory to generate a scene configuration
if (route) {
return NavigatorSceneConfigFactory_1.NavigatorSceneConfigFactory.createConfig(route.sceneConfigType);
}
return undefined;
};
// Render a scene for the navigator
NavigatorImpl.prototype._renderNavigatorScene = function (route, index) {
var _this = this;
var styles = [_styles.baseScene, _styles.sceneStyle,
_styles.defaultSceneStyle];
if (index !== this.state.presentedIndex) {
// update styles
styles.push(_styles.disabledScene);
}
// Wraps the callback passed as a prop to Navigator to render the scene
return (React.createElement(reactxp_1.View, { key: 'scene_' + this._getRouteID(route), ref: function (comp) { return _this._onMountScene(comp, index); }, style: styles }, this.props.renderScene(route)));
};
NavigatorImpl.prototype._onMountScene = function (comp, sceneIndex) {
var sceneId = 'scene_' + sceneIndex;
if (!comp) {
delete this._sceneRefs[sceneId];
}
else {
this._sceneRefs[sceneId] = comp;
}
};
// Push a scene below the others so they don't block touches sent to the presented scenes.
NavigatorImpl.prototype._disableScene = function (sceneIndex) {
var sceneId = 'scene_' + sceneIndex;
if (this._sceneRefs[sceneId]) {
this._setNativeStyles(this._sceneRefs[sceneId], {
opacity: 0,
zIndex: -10
});
}
};
// Add styles on the scene - At this time, the scene should be mounted and sitting in the
// DOM. We are just adding giving styles to this current scene.
NavigatorImpl.prototype._enableScene = function (sceneIndex, force) {
if (force === void 0) { force = false; }
var sceneStyle = reactxp_1.Styles.combine([_styles.baseScene, _styles.sceneStyle, _styles.defaultSceneStyle]);
// Then restore the top value for this scene.
var enabledSceneNativeProps = {
style: {
top: sceneStyle['top'],
bottom: sceneStyle['bottom'],
opacity: 1,
zIndex: 0,
transform: ''
}
};
if (!force && sceneIndex !== this.state.transitionFromIndex &&
sceneIndex !== this.state.presentedIndex) {
// If we are not in a transition from this index, make sure opacity is 0 to prevent the enabled scene from
// flashing over the presented scene.
enabledSceneNativeProps.style.opacity = 0;
}
var sceneId = 'scene_' + sceneIndex;
if (this._sceneRefs[sceneId]) {
this._setNativeStyles(this._sceneRefs[sceneId], enabledSceneNativeProps.style);
}
};
NavigatorImpl.prototype._transitionTo = function (destIndex, velocity, jumpSpringTo, cb) {
// If we're already presenting this index, bail here.
if (destIndex === this.state.presentedIndex) {
return;
}
// If we're already transitioning to another index, queue this one.
if (this.state.transitionFromIndex !== undefined) {
var newTransitionQueue = _.cloneDeep(this.state.transitionQueue);
newTransitionQueue.push({
destIndex: destIndex,
velocity: velocity,
transitionFinished: cb
});
// set new transition queue
this.setState({ transitionQueue: newTransitionQueue });
return;
}
// Set new state values.
this.setState({
transitionFromIndex: this.state.presentedIndex,
presentedIndex: destIndex,
transitionFinished: cb
});
// Grab the scene config from the route we're leaving.
var sceneConfig = this.state.sceneConfigStack[this.state.transitionFromIndex] ||
this.state.sceneConfigStack[this.state.presentedIndex];
this._invariant(!!sceneConfig, 'Cannot configure scene at index ' + this.state.transitionFromIndex);
// Set the spring in motion. Updates will trigger _handleSpringUpdate.
if (jumpSpringTo !== undefined) {
this.spring.setCurrentValue(jumpSpringTo);
}
this.spring.setOvershootClampingEnabled(true);
this.spring.getSpringConfig().friction = sceneConfig.springFriction;
this.spring.getSpringConfig().tension = sceneConfig.springTension;
this.spring.setVelocity(velocity || sceneConfig.defaultTransitionVelocity);
this.spring.setEndValue(1);
if (this.props.transitionStarted) {
this.props.transitionStarted();
}
};
NavigatorImpl.prototype._completeTransition = function () {
var newState = {};
this.setState({
transitionFromIndex: undefined
});
this.spring.setCurrentValue(0).setAtRest();
this._hideScenes();
// Do we have pending transitions? trigger transitions then
if (this.state.transitionQueue.length) {
var newTransitionQueue = _.cloneDeep(this.state.transitionQueue);
var queuedTransition = newTransitionQueue.shift();
// add styles on the scene we are about to transition to
this._enableScene(queuedTransition.destIndex);
this._transitionTo(queuedTransition.destIndex, queuedTransition.velocity, undefined, queuedTransition.transitionFinished);
if (this.state.transitionFinished) {
this.state.transitionFinished();
newState.transitionFinished = undefined;
}
newState.transitionQueue = newTransitionQueue;
// New setState
this.setState(newState);
}
else {
if (this.props.transitionCompleted) {
this.props.transitionCompleted();
}
if (this.state.transitionFinished) {
this.state.transitionFinished();
}
}
};
NavigatorImpl.prototype._hideScenes = function () {
for (var i = 0; i < this.state.routeStack.length; i++) {
if (i === this.state.presentedIndex ||
i === this.state.transitionFromIndex) {
continue;
}
this._disableScene(i);
}
};
// This happens for each frame of either a gesture or a transition. If both are happening, we only set values for
// the transition and the gesture will catch up later.
NavigatorImpl.prototype._handleSpringUpdate = function () {
// Prioritize handling transition in progress over a gesture:
if (this.state.transitionFromIndex !== undefined) {
this._transitionBetween(this.state.transitionFromIndex, this.state.presentedIndex, this.spring.getCurrentValue());
}
};
NavigatorImpl.prototype._transitionSceneStyle = function (fromIndex, toIndex, progress, index) {
var sceneId = 'scene_' + index;
var viewAtIndex = this._sceneRefs[sceneId];
if (viewAtIndex === undefined) {
return;
}
// Use toIndex animation when we move forwards. Use fromIndex when we move back.
var sceneConfigIndex = fromIndex < toIndex ? toIndex : fromIndex;
var sceneConfig = this.state.sceneConfigStack[sceneConfigIndex];
// This happens for overswiping when there is no scene at toIndex.
if (!sceneConfig) {
sceneConfig = this.state.sceneConfigStack[sceneConfigIndex - 1];
}
var styleToUse = {};
var useFn = index < fromIndex || index < toIndex ?
sceneConfig.animationInterpolators.out :
sceneConfig.animationInterpolators.into;
var directionAdjustedProgress = fromIndex < toIndex ? progress : 1 - progress;
var didChange = useFn(styleToUse, this._dimensions, directionAdjustedProgress);
if (didChange) {
this._setNativeStyles(viewAtIndex, styleToUse);
}
};
NavigatorImpl.prototype._transitionBetween = function (fromIndex, toIndex, progress) {
this._transitionSceneStyle(fromIndex, toIndex, progress, fromIndex);
this._transitionSceneStyle(fromIndex, toIndex, progress, toIndex);
};
NavigatorImpl.prototype._getDestIndexWithinBounds = function (n) {
var currentIndex = this.state.presentedIndex;
var destIndex = currentIndex + n;
this._invariant(destIndex >= 0, 'Cannot jump before the first route.');
var maxIndex = this.state.routeStack.length - 1;
this._invariant(maxIndex >= destIndex, 'Cannot jump past the last route.');
return destIndex;
};
NavigatorImpl.prototype._jumpN = function (n) {
var destIndex = this._getDestIndexWithinBounds(n);
this._invariant(destIndex !== -1, 'Cannot jump to route that is not in the route stack');
this._enableScene(destIndex);
this._transitionTo(destIndex);
};
NavigatorImpl.prototype._popN = function (n) {
var _this = this;
if (n === 0) {
return;
}
this._invariant(this.state.presentedIndex - n >= 0, 'Cannot pop below zero');
var popIndex = this.state.presentedIndex - n;
this._enableScene(popIndex);
this._transitionTo(popIndex, undefined, // default velocity
undefined, // no spring jumping
function () {
_this._cleanScenesPastIndex(popIndex);
});
};
NavigatorImpl.prototype._cleanScenesPastIndex = function (index) {
var newStackLength = index + 1;
// Remove any unneeded rendered routes.
if (newStackLength < this.state.routeStack.length) {
this.setState({
sceneConfigStack: this.state.sceneConfigStack.slice(0, newStackLength),
routeStack: this.state.routeStack.slice(0, newStackLength)
});
}
};
// Get routeId for the incoming route
NavigatorImpl.prototype._getRouteID = function (route) {
return route.routeId;
};
// Define an inconstiant method like React.Navigator
NavigatorImpl.prototype._invariant = function (test, failureMessage) {
if (!test) {
throw failureMessage;
}
};
// Manually override the styles in the DOM for the given component. This method is a hacky equivalent of React Native's
// setNativeProps.
NavigatorImpl.prototype._setNativeStyles = function (component, currentStyles) {
// Grab the actual element from the DOM.
var element = ReactDOM.findDOMNode(component);
if (element) {
var flatStyles = Array.isArray(currentStyles) ? _.flatten(currentStyles) : currentStyles;
// Modify styles
_.assign(element.style, flatStyles);
}
};
return NavigatorImpl;
}(Types_1.Navigator));
exports.NavigatorImpl = NavigatorImpl;
exports.default = NavigatorImpl;
exports.Navigator = NavigatorImpl;
exports.NavigatorDelegateSelector = undefined;