react-native-timeline-flatlist
Version:
Timeline component for React Native
381 lines (380 loc) • 14.7 kB
JavaScript
import React, { PureComponent } from "react";
import { FlatList, I18nManager, Image, StyleSheet, Text, TouchableOpacity, View, } from "react-native";
// Constants
const defaultCircleSize = 16;
const defaultCircleColor = "#007AFF";
const defaultLineWidth = 2;
const defaultLineStyle = "solid";
const defaultLineColor = "#007AFF";
const defaultTimeTextColor = "black";
const defaultDotColor = "white";
const defaultInnerCircle = "none";
const isRtl = I18nManager.isRTL;
class Timeline extends PureComponent {
constructor(props) {
super(props);
this._keyExtractor = (item, index) => {
if (this.props.options?.keyExtractor) {
return this.props.options.keyExtractor(item, index);
}
return index.toString();
};
this._renderItem = ({ item, index, }) => {
let content = null;
switch (this.props.columnFormat) {
case "single-column-left":
content = (<View style={[styles.rowContainer, this.props.rowContainerStyle]}>
{this.renderTime(item, index)}
{this.renderEvent(item, index)}
{this.renderCircle(item, index)}
</View>);
break;
case "single-column-right":
content = (<View style={[styles.rowContainer, this.props.rowContainerStyle]}>
{this.renderEvent(item, index)}
{this.renderTime(item, index)}
{this.renderCircle(item, index)}
</View>);
break;
case "two-column":
content =
(item.position && item.position === "right") ||
(!item.position && index % 2 === 0) ? (<View style={[styles.rowContainer, this.props.rowContainerStyle]}>
{this.renderTime(item, index)}
{this.renderEvent(item, index)}
{this.renderCircle(item, index)}
</View>) : (<View style={[styles.rowContainer, this.props.rowContainerStyle]}>
{this.renderEvent(item, index)}
{this.renderTime(item, index)}
{this.renderCircle(item, index)}
</View>);
break;
}
return <View key={index}>{content}</View>;
};
this.renderTime = (this.props.renderTime ? this.props.renderTime : this._renderTime).bind(this);
this.renderDetail = (this.props.renderDetail ? this.props.renderDetail : this._renderDetail).bind(this);
this.renderCircle = (this.props.renderCircle ? this.props.renderCircle : this._renderCircle).bind(this);
this.renderEvent = this._renderEvent.bind(this);
this.state = {
data: this.props.data,
x: 0,
width: 0,
};
}
static getDerivedStateFromProps(nextProps, prevState) {
if (prevState.data !== nextProps.data) {
return {
data: nextProps.data,
};
}
return null;
}
render() {
return (<View style={[styles.container, this.props.style]}>
{this.props.isUsingFlatlist ? (<FlatList style={[styles.listview, this.props.listViewStyle]} contentContainerStyle={this.props.listViewContainerStyle} data={this.state.data} extraData={this.state} renderItem={this._renderItem} keyExtractor={this._keyExtractor} {...this.props.options}/>) : (<View style={[
styles.listview,
this.props.listViewStyle,
this.props.listViewContainerStyle,
]}>
{this.state.data.map((item, index) => (<View key={this._keyExtractor(item, index)}>
{this._renderItem({ item, index })}
</View>))}
</View>)}
</View>);
}
_renderTime(rowData, rowID) {
if (!this.props.showTime) {
return null;
}
let timeWrapper = null;
switch (this.props.columnFormat) {
case "single-column-left":
timeWrapper = {
alignItems: "flex-end",
};
break;
case "single-column-right":
timeWrapper = {
alignItems: "flex-start",
};
break;
case "two-column":
timeWrapper = {
flex: 1,
alignItems: (rowData.position && rowData.position === "right") ||
(!rowData.position && rowID % 2 === 0)
? "flex-end"
: "flex-start",
};
break;
}
const { isAllowFontScaling } = this.props;
return (<View style={timeWrapper}>
<View style={[styles.timeContainer, this.props.timeContainerStyle]}>
<Text style={[styles.time, this.props.timeStyle]} allowFontScaling={isAllowFontScaling}>
{rowData.time}
</Text>
</View>
</View>);
}
_renderEvent(rowData, rowID) {
const lineWidth = rowData.lineWidth ?? this.props.lineWidth;
const lineStyle = rowData.lineStyle ?? this.props.lineStyle;
const columnSideMargin = rowData.columnSideMargin ?? this.props.columnSideMargin;
const columnSidePadding = rowData.columnSidePadding ?? this.props.columnSidePadding;
const isLast = this.props.renderFullLine
? !this.props.renderFullLine
: this.state.data.slice(-1)[0] === rowData;
const lineColor = isLast
? "rgba(0,0,0,0)"
: rowData.lineColor ?? this.props.lineColor;
let opStyle = null;
switch (this.props.columnFormat) {
case "single-column-left":
opStyle = {
borderColor: lineColor,
borderLeftWidth: lineWidth,
borderStyle: lineStyle,
borderRightWidth: 0,
marginLeft: columnSideMargin,
paddingLeft: columnSidePadding,
};
break;
case "single-column-right":
opStyle = {
borderColor: lineColor,
borderLeftWidth: 0,
borderRightWidth: lineWidth,
borderStyle: lineStyle,
marginRight: columnSideMargin,
paddingRight: columnSidePadding,
};
break;
case "two-column":
opStyle =
(rowData.position && rowData.position === "right") ||
(!rowData.position && rowID % 2 === 0)
? {
borderColor: lineColor,
borderLeftWidth: lineWidth,
borderStyle: lineStyle,
borderRightWidth: 0,
marginLeft: columnSideMargin,
paddingLeft: columnSidePadding,
}
: {
borderColor: lineColor,
borderLeftWidth: 0,
borderRightWidth: lineWidth,
borderStyle: lineStyle,
marginRight: columnSideMargin,
paddingRight: columnSidePadding,
};
break;
}
return (<View style={[
styles.details,
opStyle,
this.props.eventContainerStyle,
rowData.eventContainerStyle,
]} onLayout={(evt) => {
if (!this.state.x && !this.state.width) {
const { x, width } = evt.nativeEvent.layout;
this.setState({ x, width });
}
}}>
<TouchableOpacity disabled={this.props.onEventPress == null} style={[this.props.detailContainerStyle]} onPress={() => this.props.onEventPress ? this.props.onEventPress(rowData) : null}>
<View style={[styles.detail, this.props.eventDetailStyle]}>
{this.renderDetail(rowData, rowID)}
</View>
{this._renderSeparator()}
</TouchableOpacity>
</View>);
}
_renderDetail(rowData, _rowID) {
const { isAllowFontScaling } = this.props;
let description;
if (typeof rowData.description === "string") {
description = (<Text style={[
styles.description,
this.props.descriptionStyle,
rowData.descriptionStyle,
]} allowFontScaling={isAllowFontScaling}>
{rowData.description}
</Text>);
}
else if (typeof rowData.description === "object") {
description = rowData.description;
}
return (<View style={styles.container}>
<Text style={[styles.title, this.props.titleStyle, rowData.titleStyle]} allowFontScaling={isAllowFontScaling}>
{rowData.title}
</Text>
{description}
</View>);
}
_renderCircle(rowData, _rowID) {
const circleSize = rowData.circleSize ?? this.props.circleSize ?? defaultCircleSize;
const circleColor = rowData.circleColor ?? this.props.circleColor ?? defaultCircleColor;
const lineWidth = rowData.lineWidth ?? this.props.lineWidth ?? defaultLineWidth;
let circleStyle = null;
switch (this.props.columnFormat) {
case "single-column-left":
circleStyle = isRtl
? {
width: this.state.width ? circleSize : 0,
height: this.state.width ? circleSize : 0,
borderRadius: circleSize / 2,
backgroundColor: circleColor,
right: this.state.width - circleSize / 2 - (lineWidth - 1) / 2,
}
: {
width: this.state.x ? circleSize : 0,
height: this.state.x ? circleSize : 0,
borderRadius: circleSize / 2,
backgroundColor: circleColor,
left: this.state.x - circleSize / 2 + (lineWidth - 1) / 2,
};
break;
case "single-column-right":
circleStyle = {
width: this.state.width ? circleSize : 0,
height: this.state.width ? circleSize : 0,
borderRadius: circleSize / 2,
backgroundColor: circleColor,
left: this.state.width - circleSize / 2 - (lineWidth - 1) / 2,
};
break;
case "two-column":
circleStyle = {
width: this.state.width ? circleSize : 0,
height: this.state.width ? circleSize : 0,
borderRadius: circleSize / 2,
backgroundColor: circleColor,
left: this.state.width - circleSize / 2 - (lineWidth - 1) / 2,
};
break;
}
let innerCircle = null;
switch (this.props.innerCircle) {
case "icon": {
const iconDefault = rowData.iconDefault ?? this.props.iconDefault;
let iconSource = rowData.icon ?? iconDefault;
if (React.isValidElement(iconSource)) {
innerCircle = iconSource;
break;
}
if (rowData.icon) {
iconSource =
typeof rowData.icon === "string"
? { uri: rowData.icon }
: rowData.icon;
}
const iconStyle = {
height: circleSize,
width: circleSize,
};
innerCircle = (<Image source={iconSource} defaultSource={typeof iconDefault === "number" ? iconDefault : undefined} style={[iconStyle, this.props.iconStyle]}/>);
break;
}
case "dot": {
const dotSize = this.props.dotSize ?? circleSize / 2;
const dotStyle = {
height: dotSize,
width: dotSize,
borderRadius: circleSize / 4,
backgroundColor: rowData.dotColor ?? this.props.dotColor ?? defaultDotColor,
};
innerCircle = <View style={[styles.dot, dotStyle]}/>;
break;
}
case "element":
innerCircle = rowData.icon;
break;
}
return (<View style={[styles.circle, circleStyle, this.props.circleStyle]}>
{innerCircle}
</View>);
}
_renderSeparator() {
if (!this.props.separator) {
return null;
}
return <View style={[styles.separator, this.props.separatorStyle]}/>;
}
}
Timeline.defaultProps = {
circleSize: defaultCircleSize,
circleColor: defaultCircleColor,
lineWidth: defaultLineWidth,
lineStyle: defaultLineStyle,
lineColor: defaultLineColor,
innerCircle: defaultInnerCircle,
columnFormat: "single-column-left",
separator: false,
showTime: true,
isAllowFontScaling: true,
isUsingFlatlist: true,
columnSideMargin: 20,
columnSidePadding: 20,
};
export default Timeline;
const styles = StyleSheet.create({
container: {
flex: 1,
},
listview: {
flex: 1,
},
rowContainer: {
flexDirection: "row",
flex: 1,
justifyContent: "center",
},
timeContainer: {
minWidth: 45,
},
time: {
textAlign: "right",
color: defaultTimeTextColor,
overflow: "hidden",
},
circle: {
width: 16,
height: 16,
borderRadius: 10,
zIndex: 1,
position: "absolute",
alignItems: "center",
justifyContent: "center",
},
dot: {
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: defaultDotColor,
},
title: {
fontSize: 16,
fontWeight: "bold",
},
details: {
borderLeftWidth: defaultLineWidth,
flexDirection: "column",
flex: 1,
},
detail: {
paddingTop: 10,
paddingBottom: 10,
},
description: {
marginTop: 10,
},
separator: {
height: 1,
backgroundColor: "#aaa",
marginTop: 10,
marginBottom: 10,
},
});