react-native/Libraries/ReactIOS/WarningBox.js

394 lines
11 KiB
JavaScript
Raw Normal View History

/**
* 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 WarningBox
*/
'use strict';
var AsyncStorage = require('AsyncStorage');
var EventEmitter = require('EventEmitter');
var Map = require('Map');
var PanResponder = require('PanResponder');
var React = require('React');
var StyleSheet = require('StyleSheet');
var Text = require('Text');
var TouchableOpacity = require('TouchableOpacity');
var View = require('View');
var invariant = require('invariant');
var rebound = require('rebound');
var stringifySafe = require('stringifySafe');
var SCREEN_WIDTH = require('Dimensions').get('window').width;
var IGNORED_WARNINGS_KEY = '__DEV_WARNINGS_IGNORED';
var consoleWarn = console.warn.bind(console);
var warningCounts = new Map();
var ignoredWarnings: Array<string> = [];
var totalWarningCount = 0;
var warningCountEvents = new EventEmitter();
/**
* WarningBox renders warnings on top of the app being developed. Warnings help
* guard against subtle yet significant issues that can impact the quality of
* your application, such as accessibility and memory leaks. This "in your
* face" style of warning allows developers to notice and correct these issues
* as quickly as possible.
*
* The warning box is currently opt-in. Set the following flag to enable it:
*
* `console.yellowBoxEnabled = true;`
*
* If "ignore" is tapped on a warning, the WarningBox will record that warning
* and will not display it again. This is useful for hiding errors that already
* exist or have been introduced elsewhere. To re-enable all of the errors, set
* the following:
*
* `console.yellowBoxResetIgnored = true;`
*
* This can also be set permanently, and ignore will only silence the warnings
* until the next refresh.
*/
if (__DEV__) {
console.warn = function() {
consoleWarn.apply(null, arguments);
if (!console.yellowBoxEnabled) {
return;
}
var warning = Array.prototype.map.call(arguments, stringifySafe).join(' ');
if (!console.yellowBoxResetIgnored &&
ignoredWarnings.indexOf(warning) !== -1) {
return;
}
var count = warningCounts.has(warning) ? warningCounts.get(warning) + 1 : 1;
warningCounts.set(warning, count);
totalWarningCount += 1;
warningCountEvents.emit('count', totalWarningCount);
};
}
function saveIgnoredWarnings() {
AsyncStorage.setItem(
IGNORED_WARNINGS_KEY,
JSON.stringify(ignoredWarnings),
function(err) {
if (err) {
console.warn('Could not save ignored warnings.', err);
}
}
);
}
AsyncStorage.getItem(IGNORED_WARNINGS_KEY, function(err, data) {
if (!err && data && !console.yellowBoxResetIgnored) {
ignoredWarnings = JSON.parse(data);
}
});
var WarningRow = React.createClass({
componentWillMount: function() {
this.springSystem = new rebound.SpringSystem();
this.dismissalSpring = this.springSystem.createSpring();
this.dismissalSpring.setRestSpeedThreshold(0.05);
this.dismissalSpring.setCurrentValue(0);
this.dismissalSpring.addListener({
onSpringUpdate: () => {
var val = this.dismissalSpring.getCurrentValue();
this.text && this.text.setNativeProps({
left: SCREEN_WIDTH * val,
});
this.container && this.container.setNativeProps({
opacity: 1 - val,
});
this.closeButton && this.closeButton.setNativeProps({
opacity: 1 - (val * 5),
});
},
onSpringAtRest: () => {
if (this.dismissalSpring.getCurrentValue()) {
this.collapseSpring.setEndValue(1);
}
},
});
this.collapseSpring = this.springSystem.createSpring();
this.collapseSpring.setRestSpeedThreshold(0.05);
this.collapseSpring.setCurrentValue(0);
this.collapseSpring.getSpringConfig().friction = 20;
this.collapseSpring.getSpringConfig().tension = 200;
this.collapseSpring.addListener({
onSpringUpdate: () => {
var val = this.collapseSpring.getCurrentValue();
this.container && this.container.setNativeProps({
height: Math.abs(46 - (val * 46)),
});
},
onSpringAtRest: () => {
this.props.onDismissed();
},
});
this.panGesture = PanResponder.create({
onStartShouldSetPanResponder: () => {
return !! this.dismissalSpring.getCurrentValue();
},
onMoveShouldSetPanResponder: () => true,
onPanResponderGrant: () => {
this.isResponderOnlyToBlockTouches =
!! this.dismissalSpring.getCurrentValue();
},
onPanResponderMove: (e, gestureState) => {
if (this.isResponderOnlyToBlockTouches) {
return;
}
this.dismissalSpring.setCurrentValue(gestureState.dx / SCREEN_WIDTH);
},
onPanResponderRelease: (e, gestureState) => {
if (this.isResponderOnlyToBlockTouches) {
return;
}
var gestureCompletion = gestureState.dx / SCREEN_WIDTH;
var doesGestureRelease = (gestureState.vx + gestureCompletion) > 0.5;
this.dismissalSpring.setEndValue(doesGestureRelease ? 1 : 0);
}
});
},
render: function() {
var countText;
if (warningCounts.get(this.props.warning) > 1) {
countText = (
<Text style={styles.bold}>
({warningCounts.get(this.props.warning)}){" "}
</Text>
);
}
return (
<View
style={styles.warningBox}
ref={container => { this.container = container; }}
{...this.panGesture.panHandlers}>
<TouchableOpacity
onPress={this.props.onOpened}>
[Animated][BREAKING_CHANGE] Convert <TouchableOpacity> to Animated Summary: Because we don't want to integrate Animated inside of the core of React, we can only pass Animated.Value to styles of <Animated.View>. TouchableOpacity unfortunately used cloneElement. This means that we should have asked every single call site to replace their children to Animated.View. This isn't great. The other solution is to stop using cloneElement and instead wrap the children inside of an <Animated.View>. This has many advantages: - We no longer use cloneElement so we're no longer messing up with elements that are not our own. - Refs are now working correctly for children elements - No longer need to enforce that there's only one child and that this child is a native element The downside is that we're introducing a <View> into the hierarchy. Sadly with CSS there is no way to have a View that doesn't affect layout. What we need to do is to remove the inner <View> and transfer all the styles to the TouchableOpacity. It is annoying but fortunately a pretty mechanical process. I think that having a wrapper is the best solution. I will investigate to see if we can make wrappers on TouchableHighliht and TouchableWithoutFeedback as well. **Upgrade Path:** If the child is a View, move the style of the View to TouchableOpacity and remove the View itself. ``` <TouchableOpacity onPress={...}> <View style={...}> ... </View> </TouchableOpacity> --> <TouchableOpacity onPress={...} style={...}> ... </TouchableOpacity> ``` If the child is an Image or Text, on all the examples at Facebook it worked without any change. But it is a great idea to double check them anyway.
2015-07-20 23:29:40 +00:00
<Text
style={styles.warningText}
numberOfLines={2}
ref={text => { this.text = text; }}>
{countText}
{this.props.warning}
</Text>
</TouchableOpacity>
<View
ref={closeButton => { this.closeButton = closeButton; }}
style={styles.closeButton}>
<TouchableOpacity
onPress={() => {
this.dismissalSpring.setEndValue(1);
}}>
<Text style={styles.closeButtonText}></Text>
</TouchableOpacity>
</View>
</View>
);
}
});
var WarningBoxOpened = React.createClass({
render: function() {
var countText;
if (warningCounts.get(this.props.warning) > 1) {
countText = (
<Text style={styles.bold}>
({warningCounts.get(this.props.warning)}){" "}
</Text>
);
}
return (
<TouchableOpacity
activeOpacity={0.9}
[Animated][BREAKING_CHANGE] Convert <TouchableOpacity> to Animated Summary: Because we don't want to integrate Animated inside of the core of React, we can only pass Animated.Value to styles of <Animated.View>. TouchableOpacity unfortunately used cloneElement. This means that we should have asked every single call site to replace their children to Animated.View. This isn't great. The other solution is to stop using cloneElement and instead wrap the children inside of an <Animated.View>. This has many advantages: - We no longer use cloneElement so we're no longer messing up with elements that are not our own. - Refs are now working correctly for children elements - No longer need to enforce that there's only one child and that this child is a native element The downside is that we're introducing a <View> into the hierarchy. Sadly with CSS there is no way to have a View that doesn't affect layout. What we need to do is to remove the inner <View> and transfer all the styles to the TouchableOpacity. It is annoying but fortunately a pretty mechanical process. I think that having a wrapper is the best solution. I will investigate to see if we can make wrappers on TouchableHighliht and TouchableWithoutFeedback as well. **Upgrade Path:** If the child is a View, move the style of the View to TouchableOpacity and remove the View itself. ``` <TouchableOpacity onPress={...}> <View style={...}> ... </View> </TouchableOpacity> --> <TouchableOpacity onPress={...} style={...}> ... </TouchableOpacity> ``` If the child is an Image or Text, on all the examples at Facebook it worked without any change. But it is a great idea to double check them anyway.
2015-07-20 23:29:40 +00:00
onPress={this.props.onClose}
style={styles.yellowBox}>
<Text style={styles.yellowBoxText}>
{countText}
{this.props.warning}
</Text>
<View style={styles.yellowBoxButtons}>
<TouchableOpacity
onPress={this.props.onDismissed}
style={styles.yellowBoxButton}>
<Text style={styles.yellowBoxButtonText}>
Dismiss
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={this.props.onIgnored}
style={styles.yellowBoxButton}>
<Text style={styles.yellowBoxButtonText}>
Ignore
</Text>
</TouchableOpacity>
</View>
</TouchableOpacity>
);
},
});
var canMountWarningBox = true;
var WarningBox = React.createClass({
getInitialState: function() {
return {
totalWarningCount,
openWarning: null,
};
},
componentWillMount: function() {
if (console.yellowBoxResetIgnored) {
AsyncStorage.setItem(IGNORED_WARNINGS_KEY, '[]', function(err) {
if (err) {
console.warn('Could not reset ignored warnings.', err);
}
});
ignoredWarnings = [];
}
},
componentDidMount: function() {
invariant(
canMountWarningBox,
'There can only be one WarningBox'
);
canMountWarningBox = false;
warningCountEvents.addListener(
'count',
this._onWarningCount
);
},
componentWillUnmount: function() {
warningCountEvents.removeAllListeners();
canMountWarningBox = true;
},
_onWarningCount: function(totalWarningCount) {
// Must use setImmediate because warnings often happen during render and
// state cannot be set while rendering
setImmediate(() => {
this.setState({ totalWarningCount, });
});
},
_onDismiss: function(warning) {
warningCounts.delete(warning);
this.setState({
openWarning: null,
});
},
render: function() {
if (warningCounts.size === 0) {
return <View />;
}
if (this.state.openWarning) {
return (
<WarningBoxOpened
warning={this.state.openWarning}
onClose={() => {
this.setState({ openWarning: null });
}}
onDismissed={this._onDismiss.bind(this, this.state.openWarning)}
onIgnored={() => {
ignoredWarnings.push(this.state.openWarning);
saveIgnoredWarnings();
this._onDismiss(this.state.openWarning);
}}
/>
);
}
var warningRows = [];
warningCounts.forEach((count, warning) => {
warningRows.push(
<WarningRow
key={warning}
onOpened={() => {
this.setState({ openWarning: warning });
}}
onDismissed={this._onDismiss.bind(this, warning)}
warning={warning}
/>
);
});
return (
<View style={styles.warningContainer}>
{warningRows}
</View>
);
},
});
var styles = StyleSheet.create({
bold: {
fontWeight: 'bold',
},
closeButton: {
position: 'absolute',
right: 0,
height: 46,
width: 46,
},
closeButtonText: {
color: 'white',
fontSize: 32,
position: 'relative',
left: 8,
},
warningContainer: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0
},
warningBox: {
position: 'relative',
backgroundColor: 'rgba(171, 124, 36, 0.9)',
flex: 1,
height: 46,
},
warningText: {
color: 'white',
position: 'absolute',
left: 0,
marginLeft: 15,
marginRight: 46,
top: 7,
},
yellowBox: {
backgroundColor: 'rgba(171, 124, 36, 0.9)',
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
padding: 15,
paddingTop: 35,
},
yellowBoxText: {
color: 'white',
fontSize: 20,
},
yellowBoxButtons: {
flexDirection: 'row',
position: 'absolute',
bottom: 0,
},
yellowBoxButton: {
flex: 1,
padding: 25,
},
yellowBoxButtonText: {
color: 'white',
fontSize: 16,
}
});
module.exports = WarningBox;