react-native-message-bar
Version:
A message bar alert displayed at the top of the screen for react-native
543 lines (486 loc) • 16.9 kB
JavaScript
/**
* Name: MessageBar
* Description: A Message Bar Component displayed at the top of screen
* https://github.com/talor-a/react-native-message-bar
*/
'use strict'
import React, { Component } from 'react'
import {
Text,
View,
TouchableOpacity,
Animated,
Dimensions,
Image
} from 'react-native'
let windowWidth = Dimensions.get('window').width
let windowHeight = Dimensions.get('window').height
class MessageBar extends Component {
constructor (props) {
super(props)
this.animatedValue = new Animated.Value(0)
this.notifyAlertHiddenCallback = null
this.alertShown = false
this.timeoutHide = null
this.state = this.getStateByProps(props)
this.defaultState = this.getStateByProps(props)
}
componentDidMount () {
// Configure the offsets prior to recieving updated props or recieving the first alert
// This ensures the offsets are set properly at the outset based on the initial position.
// This prevents the bar from appearing and covering half of the screen when the
// device is started in landscape and then rotated to portrait.
// This does not happen after the first alert appears, as setNewState() is called on each
// alert and calls _changeOffsetByPosition()
this._changeOffsetByPosition(this.state.position)
}
UNSAFE_componentWillReceiveProps (nextProps) {
if (nextProps && Object.keys(nextProps).length > 0) {
this.setNewState(nextProps)
}
}
setNewState (state) {
// Set the new state, this is triggered when the props of this MessageBar changed
this.setState(this.getStateByProps(state))
// Apply the colors of the alert depending on its alertType
this._applyAlertStylesheet(state.alertType)
// Override the opposition style position regarding the state position in order to have the alert sticks that position
this._changeOffsetByPosition(state.position)
}
getStateByProps (props) {
const def = this.defaultState || {}
return {
// Default values, will be overridden
backgroundColor: '#007bff', // default value : blue
strokeColor: '#006acd', // default value : blue
titleColor: '#ffffff', // default value : white
messageColor: '#ffffff', // default value : white
animationTypeTransform: 'SlideFromTop', // default value
/* Cusomisation of the alert: Title, Message, Icon URL, Alert alertType (error, success, warning, info), Duration for Alert keep shown */
title: props.title,
message: props.message,
children: props.children,
avatar: props.avatar,
alertType: props.alertType || 'info',
duration: props.duration || def.duration || 3000,
/* Hide setters */
get shouldHideAfterDelay () {
if (props.shouldHideAfterDelay != undefined) { return props.shouldHideAfterDelay }
if (def.shouldHideAfterDelay != undefined) { return def.shouldHideAfterDelay }
return true
},
shouldHideOnTap: props.shouldHideOnTap == undefined &&
def.shouldHideOnTap == undefined
? true
: props.shouldHideOnTap || def.shouldHideOnTap,
/* Callbacks method on Alert Tapped, on Alert Show, on Alert Hide */
onTapped: props.onTapped || def.onTapped,
onShow: props.onShow || def.onShow,
onHide: props.onHide || def.onHide,
/* Stylesheets */
stylesheetInfo: props.stylesheetInfo ||
def.stylesheetInfo || {
backgroundColor: '#007bff',
strokeColor: '#006acd',
titleColor: '#ffffff',
messageColor: '#ffffff'
}, // Default are blue colors
stylesheetSuccess: props.stylesheetSuccess ||
def.stylesheetSuccess || {
backgroundColor: 'darkgreen',
strokeColor: 'darkgreen',
titleColor: '#ffffff',
messageColor: '#ffffff'
}, // Default are Green colors
stylesheetWarning: props.stylesheetWarning ||
def.stylesheetWarning || {
backgroundColor: '#ff9c00',
strokeColor: '#f29400',
titleColor: '#ffffff',
messageColor: '#ffffff'
}, // Default are orange colors
stylesheetError: props.stylesheetError ||
def.stylesheetError || {
backgroundColor: '#ff3232',
strokeColor: '#FF0000',
titleColor: '#ffffff',
messageColor: '#ffffff'
}, // Default are red colors
stylesheetExtra: props.stylesheetExtra ||
def.stylesheetExtra || {
backgroundColor: '#007bff',
strokeColor: '#006acd',
titleColor: '#ffffff',
messageColor: '#ffffff'
}, // Default are blue colors, same as info
/* Duration of the animation */
durationToShow: props.durationToShow || def.durationToShow || 350,
durationToHide: props.durationToHide || def.durationToHide || 350,
/* Offset of the View, useful if you have a navigation bar or if you want the alert be shown below another component instead of the top of the screen */
viewTopOffset: props.viewTopOffset || def.viewTopOffset || 0,
viewBottomOffset: props.viewBottomOffset || def.viewBottomOffset || 0,
viewLeftOffset: props.viewLeftOffset || def.viewLeftOffset || 0,
viewRightOffset: props.viewRightOffset || def.viewRightOffset || 0,
/* Inset of the view, useful if you want to apply a padding at your alert content */
viewTopInset: props.viewTopInset || def.viewTopInset || 0,
viewBottomInset: props.viewBottomInset || def.viewBottomInset || 0,
viewLeftInset: props.viewLeftInset || def.viewLeftInset || 0,
viewRightInset: props.viewRightInset || def.viewRightInset || 0,
/* Padding around the content, useful if you want a tiny message bar */
messageBarPadding: props.messageBarPadding || def.messageBarPadding || 10,
/* Number of Lines for Title and Message */
titleNumberOfLines:
props.titleNumberOfLines == undefined &&
def.titleNumberOfLines == undefined
? 1
: props.titleNumberOfLines || def.titleNumberOfLines,
messageNumberOfLines:
props.messageNumberOfLines == undefined &&
def.titleNumberOfLines == undefined
? 2
: props.messageNumberOfLines || def.messageNumberOfLines,
/* Style for the text elements and the avatar */
titleStyle: props.titleStyle || def.titleStyle || {
fontSize: 18,
fontWeight: 'bold'
},
messageStyle: props.messageStyle || def.messageStyle || {
fontSize: 16
},
avatarStyle: props.avatarStyle || def.avatarStyle || {
height: 40,
width: 40,
borderRadius: 20
},
/* Position of the alert and Animation Type the alert is shown */
position: props.position || def.position || 'top',
animationType: props.animationType || def.animationType
}
}
/*
* Show the alert
*/
showMessageBarAlert () {
// If an alert is already shonw or doesn't have a title or a message, do nothing
if (
this.alertShown ||
(this.state.title == null && this.state.message == null && this.state.children == null)
) {
return
}
// Set the data of the alert in the state
this.alertShown = true
// Display the alert by animating it from the top of the screen
// Auto-Hide it after a delay set in the state
Animated.timing(this.animatedValue, {
toValue: 1,
duration: this.state.durationToShow,
useNativeDriver: true
}).start(this._showMessageBarAlertComplete())
}
/*
* Hide the alert after a delay, typically used for auto-hidding
*/
_showMessageBarAlertComplete () {
// Execute onShow callback if any
this._onShow()
// If the duration is null, do not hide the
if (this.state.shouldHideAfterDelay) {
this.timeoutHide = setTimeout(() => {
this.hideMessageBarAlert()
}, this.state.duration)
}
}
/*
* Return true if the MessageBar is currently displayed, otherwise false
*/
isMessageBarShown () {
return this.alertShown
}
/*
* Hide the alert, typically used when user tap the alert
*/
hideMessageBarAlert () {
// Hide the alert after a delay set in the state only if the alert is still visible
if (!this.alertShown) {
return
}
clearTimeout(this.timeoutHide)
// Animate the alert to hide it to the top of the screen
Animated.timing(this.animatedValue, {
toValue: 0,
duration: this.state.durationToHide,
useNativeDriver: true
}).start(this._hideMessageBarAlertComplete())
}
_hideMessageBarAlertComplete () {
// The alert is not shown anymore
this.alertShown = false
this._notifyAlertHidden()
// Execute onHide callback if any
this._onHide()
}
/*
* Callback executed to tell the observer the alert is hidden
*/
_notifyAlertHidden () {
if (this.notifyAlertHiddenCallback) {
this.notifyAlertHiddenCallback()
}
}
/*
* Callback executed when the user tap the alert
*/
_alertTapped () {
// Hide the alert
if (this.state.shouldHideOnTap) {
this.hideMessageBarAlert()
}
// Execute the callback passed in parameter
if (this.state.onTapped) {
this.state.onTapped()
}
}
/*
* Callback executed when alert is shown
*/
_onShow () {
if (this.state.onShow) {
this.state.onShow()
}
}
/*
* Callback executed when alert is hidden
*/
_onHide () {
if (this.state.onHide) {
this.state.onHide()
}
}
/*
* Change the background color and the line stroke color depending on the alertType
* If the alertType is not recognized, the 'info' one (blue colors) is selected for you
*/
_applyAlertStylesheet (alertType) {
// Set the Background color and the line stroke color of the alert depending on its alertType
// Set to blue-info if no alertType or if the alertType is not recognized
let backgroundColor
let strokeColor
let titleColor
let messageColor
switch (alertType) {
case 'success':
backgroundColor = this.state.stylesheetSuccess.backgroundColor
strokeColor = this.state.stylesheetSuccess.strokeColor
titleColor = this.state.stylesheetSuccess.titleColor
messageColor = this.state.stylesheetSuccess.messageColor
break
case 'error':
backgroundColor = this.state.stylesheetError.backgroundColor
strokeColor = this.state.stylesheetError.strokeColor
titleColor = this.state.stylesheetError.titleColor
messageColor = this.state.stylesheetError.messageColor
break
case 'warning':
backgroundColor = this.state.stylesheetWarning.backgroundColor
strokeColor = this.state.stylesheetWarning.strokeColor
titleColor = this.state.stylesheetWarning.titleColor
messageColor = this.state.stylesheetWarning.messageColor
break
case 'info':
backgroundColor = this.state.stylesheetInfo.backgroundColor
strokeColor = this.state.stylesheetInfo.strokeColor
titleColor = this.state.stylesheetInfo.titleColor
messageColor = this.state.stylesheetInfo.messageColor
break
default:
backgroundColor = this.state.stylesheetExtra.backgroundColor
strokeColor = this.state.stylesheetExtra.strokeColor
titleColor = this.state.stylesheetExtra.titleColor
messageColor = this.state.stylesheetExtra.messageColor
break
}
this.setState({
backgroundColor: backgroundColor,
strokeColor: strokeColor,
titleColor: titleColor,
messageColor: messageColor
})
}
/*
* Change view<Position>Offset property depending on the state position
*/
_changeOffsetByPosition (position) {
switch (position) {
case 'top':
this.setState({
viewBottomOffset: null
})
break
case 'bottom':
this.setState({
viewTopOffset: null
})
break
default:
this.setState({
viewBottomOffset: null
})
break
}
}
/*
* Set the animation transformation depending on the chosen animationType, or depending on the state's position if animationType is not overridden
*/
_applyAnimationTypeTransformation () {
let position = this.state.position
let animationType = this.state.animationType
if (animationType === undefined) {
if (position === 'bottom') {
animationType = 'SlideFromBottom'
} else {
// Top by default
animationType = 'SlideFromTop'
}
}
switch (animationType) {
case 'SlideFromTop':
var animationY = this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [-windowHeight, 0]
})
this.animationTypeTransform = [{ translateY: animationY }]
break
case 'SlideFromBottom':
var animationY = this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [windowHeight, 0]
})
this.animationTypeTransform = [{ translateY: animationY }]
break
case 'SlideFromLeft':
var animationX = this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [-windowWidth, 0]
})
this.animationTypeTransform = [{ translateX: animationX }]
break
case 'SlideFromRight':
var animationX = this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [windowWidth, 0]
})
this.animationTypeTransform = [{ translateX: animationX }]
break
default:
var animationY = this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [-windowHeight, 0]
})
this.animationTypeTransform = [{ translateY: animationY }]
break
}
}
/*
* Alert Rendering Methods
*/
render () {
// Set the animation transformation depending on the chosen animationType, or depending on the state's position if animationType is not overridden
this._applyAnimationTypeTransformation()
return (
<Animated.View
style={{
transform: this.animationTypeTransform,
backgroundColor: this.state.backgroundColor,
borderColor: this.state.strokeColor,
borderBottomWidth: 1,
position: 'absolute',
top: this.state.viewTopOffset,
bottom: this.state.viewBottomOffset,
left: this.state.viewLeftOffset,
right: this.state.viewRightOffset,
paddingTop: this.state.viewTopInset,
paddingBottom: this.state.viewBottomInset,
paddingLeft: this.state.viewLeftInset,
paddingRight: this.state.viewRightInset
}}>
<TouchableOpacity
onPress={() => {
this._alertTapped()
}}
style={{ flex: 1 }}>
<View
style={{
flex: 1,
flexDirection: 'row',
alignItems: 'flex-end',
padding: this.state.messageBarPadding
}}>
{this.renderImage()}
<View
style={{
flex: 1,
flexDirection: 'column',
alignSelf: 'stretch',
justifyContent: 'center',
marginLeft: 10
}}>
{this.renderTitle()}
{this.renderMessage()}
</View>
</View>
</TouchableOpacity>
</Animated.View>
)
}
renderImage () {
if (this.state.avatar != null) {
var imageSource
var avatar = this.state.avatar
if (typeof avatar === 'string') {
if (avatar.match(/^https?:/)) {
// this is a network file
imageSource = { uri: avatar }
} else {
// this is a local file : require('<path/to/my/local/image.extension>')
imageSource = avatar
}
return <Image source={imageSource} style={this.state.avatarStyle} />
} else if (React.isValidElement(avatar)) {
// this is a react component
return avatar
}
}
}
renderTitle () {
if (this.state.title != null) {
return (
<Text
numberOfLines={this.state.titleNumberOfLines}
style={[this.state.titleStyle, {color: this.state.titleColor}]}>
{this.state.title}
</Text>
)
}
}
renderMessage () {
var controls = [];
if (this.state.message != null) {
controls.push(
<Text
key="message"
numberOfLines={this.state.messageNumberOfLines}
style={[this.state.messageStyle, {color: this.state.messageColor}]}>
{this.state.message}
</Text>
)
}
if (this.state.children != null) {
controls.push(
this.state.children
);
}
return controls;
}
}
module.exports = MessageBar