react-native-calendar-strip
Version:
Easy to use and visually stunning calendar component for React Native
536 lines (485 loc) • 17.4 kB
JavaScript
/**
* Created by bogdanbegovic on 8/20/16.
*/
import React, { Component } from "react";
import PropTypes from "prop-types";
import moment from "moment";
import { Text, View, Animated, Easing, LayoutAnimation, TouchableOpacity } from "react-native";
import styles from "./Calendar.style.js";
class CalendarDay extends Component {
static propTypes = {
date: PropTypes.object.isRequired,
selectedDate: PropTypes.any,
onDateSelected: PropTypes.func.isRequired,
dayComponent: PropTypes.any,
datesWhitelist: PropTypes.oneOfType([PropTypes.array, PropTypes.func]),
datesBlacklist: PropTypes.oneOfType([PropTypes.array, PropTypes.func]),
markedDates: PropTypes.oneOfType([PropTypes.array, PropTypes.func]),
showDayName: PropTypes.bool,
showDayNumber: PropTypes.bool,
calendarColor: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number,
dateNameStyle: PropTypes.any,
dateNumberStyle: PropTypes.any,
dayContainerStyle: PropTypes.any,
weekendDateNameStyle: PropTypes.any,
weekendDateNumberStyle: PropTypes.any,
highlightDateContainerStyle: PropTypes.any,
highlightDateNameStyle: PropTypes.any,
highlightDateNumberStyle: PropTypes.any,
highlightDateNumberContainerStyle: PropTypes.any,
disabledDateNameStyle: PropTypes.any,
disabledDateNumberStyle: PropTypes.any,
disabledDateOpacity: PropTypes.number,
styleWeekend: PropTypes.bool,
customDatesStyles: PropTypes.oneOfType([PropTypes.array, PropTypes.func]),
markedDatesStyle: PropTypes.object,
allowDayTextScaling: PropTypes.bool,
calendarAnimation: PropTypes.object,
registerAnimation: PropTypes.func.isRequired,
daySelectionAnimation: PropTypes.object,
useNativeDriver: PropTypes.bool,
scrollable: PropTypes.bool,
upperCaseDays: PropTypes.bool,
};
// Reference: https://medium.com/@Jpoliachik/react-native-s-layoutanimation-is-awesome-4a4d317afd3e
static defaultProps = {
daySelectionAnimation: {
type: "", // animations disabled by default
duration: 300,
borderWidth: 1,
borderHighlightColor: "black",
highlightColor: "yellow",
animType: LayoutAnimation.Types.easeInEaseOut,
animUpdateType: LayoutAnimation.Types.easeInEaseOut,
animProperty: LayoutAnimation.Properties.opacity,
animSpringDamping: undefined // Only applicable for LayoutAnimation.Types.spring,
},
styleWeekend: true,
showDayName: true,
showDayNumber: true,
upperCaseDays: true,
width: 0, // Default width and height to avoid calcSizes() *sometimes* doing Math.round(undefined) to cause NaN
height: 0
};
constructor(props) {
super(props);
this.state = {
enabled: this.isDateAllowed(props.date, props.datesBlacklist, props.datesWhitelist),
selected: this.isDateSelected(props.date, props.selectedDate),
customStyle: this.getCustomDateStyle(props.date, props.customDatesStyles),
marking: this.getDateMarking(props.date, props.markedDates),
animatedValue: new Animated.Value(0),
...this.calcSizes(props)
};
if (!props.scrollable) {
props.registerAnimation(this.createAnimation());
}
}
componentDidUpdate(prevProps, prevState) {
let newState = {};
let doStateUpdate = false;
let hasDateChanged = prevProps.date !== this.props.date;
if ((this.props.selectedDate !== prevProps.selectedDate) || hasDateChanged) {
if (this.props.daySelectionAnimation.type !== "" && !this.props.scrollable) {
let configurableAnimation = {
duration: this.props.daySelectionAnimation.duration || 300,
create: {
type:
this.props.daySelectionAnimation.animType ||
LayoutAnimation.Types.easeInEaseOut,
property:
this.props.daySelectionAnimation.animProperty ||
LayoutAnimation.Properties.opacity
},
update: {
type:
this.props.daySelectionAnimation.animUpdateType ||
LayoutAnimation.Types.easeInEaseOut,
springDamping: this.props.daySelectionAnimation.animSpringDamping
},
delete: {
type:
this.props.daySelectionAnimation.animType ||
LayoutAnimation.Types.easeInEaseOut,
property:
this.props.daySelectionAnimation.animProperty ||
LayoutAnimation.Properties.opacity
}
};
LayoutAnimation.configureNext(configurableAnimation);
}
newState.selected = this.isDateSelected(this.props.date, this.props.selectedDate);
doStateUpdate = true;
}
if (prevProps.width !== this.props.width || prevProps.height !== this.props.height) {
newState = { ...newState, ...this.calcSizes(this.props) };
doStateUpdate = true;
}
if ((prevProps.customDatesStyles !== this.props.customDatesStyles) || hasDateChanged) {
newState = { ...newState, customStyle: this.getCustomDateStyle(this.props.date, this.props.customDatesStyles) };
doStateUpdate = true;
}
if ((prevProps.markedDates !== this.props.markedDates) || hasDateChanged) {
newState = { ...newState, marking: this.getDateMarking(this.props.date, this.props.markedDates) };
doStateUpdate = true;
}
if ((prevProps.datesBlacklist !== this.props.datesBlacklist) ||
(prevProps.datesWhitelist !== this.props.datesWhitelist) ||
hasDateChanged)
{
newState = { ...newState, enabled: this.isDateAllowed(this.props.date, this.props.datesBlacklist, this.props.datesWhitelist) };
doStateUpdate = true;
}
if (doStateUpdate) {
this.setState(newState);
}
}
calcSizes = props => {
return {
containerWidth: Math.round(props.width),
containerHeight: Math.round(props.height),
containerBorderRadius: Math.round(props.width / 2),
dateNameFontSize: Math.round(props.width / 5),
dateNumberFontSize: Math.round(props.width / 2.9)
};
}
//Function to check if provided date is the same as selected one, hence date is selected
//using isSame moment query with "day" param so that it check years, months and day
isDateSelected = (date, selectedDate) => {
if (!date || !selectedDate) {
return date === selectedDate;
}
return date.isSame(selectedDate, "day");
}
// Check whether date is allowed
isDateAllowed = (date, datesBlacklist, datesWhitelist) => {
// datesBlacklist entries override datesWhitelist
if (Array.isArray(datesBlacklist)) {
for (let disallowed of datesBlacklist) {
// Blacklist start/end object
if (disallowed.start && disallowed.end) {
if (date.isBetween(disallowed.start, disallowed.end, "day", "[]")) {
return false;
}
} else {
if (date.isSame(disallowed, "day")) {
return false;
}
}
}
} else if (datesBlacklist instanceof Function) {
return !datesBlacklist(date);
}
// Whitelist
if (Array.isArray(datesWhitelist)) {
for (let allowed of datesWhitelist) {
// start/end object
if (allowed.start && allowed.end) {
if (date.isBetween(allowed.start, allowed.end, "day", "[]")) {
return true;
}
} else {
if (date.isSame(allowed, "day")) {
return true;
}
}
}
return false;
} else if (datesWhitelist instanceof Function) {
return datesWhitelist(date);
}
return true;
}
getCustomDateStyle = (date, customDatesStyles) => {
if (Array.isArray(customDatesStyles)) {
for (let customDateStyle of customDatesStyles) {
if (customDateStyle.endDate) {
// Range
if (
date.isBetween(
customDateStyle.startDate,
customDateStyle.endDate,
"day",
"[]"
)
) {
return customDateStyle;
}
} else {
// Single date
if (date.isSame(customDateStyle.startDate, "day")) {
return customDateStyle;
}
}
}
} else if (customDatesStyles instanceof Function) {
return customDatesStyles(date);
}
}
getDateMarking = (day, markedDates) => {
if (Array.isArray(markedDates)) {
if (markedDates.length === 0) {
return {};
}
return markedDates.find(md => moment(day).isSame(md.date, "day")) || {};
} else if (markedDates instanceof Function) {
return markedDates(day) || {};
}
}
createAnimation = () => {
const {
calendarAnimation,
useNativeDriver,
} = this.props
if (calendarAnimation) {
this.animation = Animated.timing(this.state.animatedValue, {
toValue: 1,
duration: calendarAnimation.duration,
easing: Easing.linear,
useNativeDriver,
});
// Individual CalendarDay animation starts have unpredictable timing
// when used with delays in RN Animated.
// Send animation to parent to collect and start together.
return this.animation;
}
}
renderMarking() {
if (!this.props.markedDates || this.props.markedDates.length === 0) {
return;
}
const marking = this.state.marking;
if (marking.dots && Array.isArray(marking.dots) && marking.dots.length > 0) {
return this.renderDots(marking);
}
if (marking.lines && Array.isArray(marking.lines) && marking.lines.length > 0) {
return this.renderLines(marking);
}
return ( // default empty spacer
<View style={styles.dotsContainer}>
<View style={[styles.dot]} />
</View>
);
}
renderDots(marking) {
const baseDotStyle = [styles.dot, styles.visibleDot];
const markedDatesStyle = this.props.markedDatesStyle || {};
const formattedDate = this.props.date.format('YYYY-MM-DD');
let validDots = <View style={[styles.dot]} />; // default empty view for no dots case
// Filter dots and process only those which have color property
validDots = marking.dots
.filter(d => (d && d.color))
.map((dot, index) => {
const selectedColor = dot.selectedColor || dot.selectedDotColor; // selectedDotColor deprecated
const backgroundColor = this.state.selected && selectedColor ? selectedColor : dot.color;
return (
<View
key={dot.key || (formattedDate + index)}
style={[
baseDotStyle,
{ backgroundColor },
markedDatesStyle
]}
/>
);
});
return (
<View style={styles.dotsContainer}>
{validDots}
</View>
);
}
renderLines(marking) {
const baseLineStyle = [styles.line, styles.visibleLine];
const markedDatesStyle = this.props.markedDatesStyle || {};
let validLines = <View style={[styles.line]} />; // default empty view
// Filter lines and process only those which have color property
validLines = marking.lines
.filter(d => (d && d.color))
.map((line, index) => {
const backgroundColor = this.state.selected && line.selectedColor ? line.selectedColor : line.color;
const width = this.props.width * 0.6;
return (
<View
key={line.key ? line.key : index}
style={[
baseLineStyle,
{ backgroundColor, width },
markedDatesStyle
]}
/>
);
});
return (
<View style={styles.linesContainer}>
{validLines}
</View>
);
}
render() {
// Defaults for disabled state
const {
date,
dateNameStyle,
dateNumberStyle,
dayContainerStyle,
disabledDateNameStyle,
disabledDateNumberStyle,
disabledDateOpacity,
calendarAnimation,
daySelectionAnimation,
highlightDateNameStyle,
highlightDateNumberStyle,
highlightDateNumberContainerStyle,
highlightDateContainerStyle,
styleWeekend,
weekendDateNameStyle,
weekendDateNumberStyle,
onDateSelected,
showDayName,
showDayNumber,
allowDayTextScaling,
dayComponent: DayComponent,
scrollable,
upperCaseDays,
} = this.props;
const {
enabled,
selected,
containerHeight,
containerWidth,
containerBorderRadius,
customStyle,
dateNameFontSize,
dateNumberFontSize,
} = this.state;
let _dateNameStyle = [styles.dateName, enabled ? dateNameStyle : disabledDateNameStyle];
let _dateNumberStyle = [styles.dateNumber, enabled ? dateNumberStyle : disabledDateNumberStyle];
let _dateViewStyle = enabled
? [{ backgroundColor: "transparent" }]
: [{ opacity: disabledDateOpacity }];
let _customHighlightDateNameStyle;
let _customHighlightDateNumberStyle;
let _dateNumberContainerStyle = [];
if (customStyle) {
_dateNameStyle.push(customStyle.dateNameStyle);
_dateNumberStyle.push(customStyle.dateNumberStyle);
_dateViewStyle.push(customStyle.dateContainerStyle);
_customHighlightDateNameStyle = customStyle.highlightDateNameStyle;
_customHighlightDateNumberStyle = customStyle.highlightDateNumberStyle;
}
if (enabled && selected) {
// Enabled state
//The user can disable animation, so that is why I use selection type
//If it is background, the user have to input colors for animation
//If it is border, the user has to input color for border animation
switch (daySelectionAnimation.type) {
case "background":
_dateViewStyle.push({ backgroundColor: daySelectionAnimation.highlightColor });
break;
case "border":
_dateViewStyle.push({
borderColor: daySelectionAnimation.borderHighlightColor,
borderWidth: daySelectionAnimation.borderWidth
});
break;
default:
// No animation styling by default
break;
}
_dateNameStyle = [styles.dateName, dateNameStyle];
_dateNumberStyle = [styles.dateNumber, dateNumberStyle];
if (styleWeekend &&
(date.isoWeekday() === 6 || date.isoWeekday() === 7)
) {
_dateNameStyle = [
styles.weekendDateName,
weekendDateNameStyle
];
_dateNumberStyle = [
styles.weekendDateNumber,
weekendDateNumberStyle
];
}
_dateViewStyle.push(highlightDateContainerStyle);
_dateNameStyle = [
styles.dateName,
highlightDateNameStyle,
_customHighlightDateNameStyle
];
_dateNumberStyle = [
styles.dateNumber,
highlightDateNumberStyle,
_customHighlightDateNumberStyle
];
_dateNumberContainerStyle.push(highlightDateNumberContainerStyle);
}
let responsiveDateContainerStyle = {
width: containerWidth,
height: containerHeight,
borderRadius: containerBorderRadius,
};
let containerStyle = selected
? { ...dayContainerStyle, ...highlightDateContainerStyle }
: dayContainerStyle;
let day;
if (DayComponent) {
day = (<DayComponent {...this.props} {...this.state}/>);
}
else {
day = (
<TouchableOpacity
onPress={onDateSelected.bind(this, date)}
disabled={!enabled}
>
<View
style={[
styles.dateContainer,
responsiveDateContainerStyle,
_dateViewStyle,
containerStyle
]}
>
{showDayName && (
<Text
style={[{ fontSize: dateNameFontSize }, _dateNameStyle]}
allowFontScaling={allowDayTextScaling}
>
{upperCaseDays ? date.format("ddd").toUpperCase() : date.format("ddd")}
</Text>
)}
{showDayNumber && (
<View style={_dateNumberContainerStyle}>
<Text
style={[
{ fontSize: dateNumberFontSize },
_dateNumberStyle
]}
allowFontScaling={allowDayTextScaling}
>
{date.date()}
</Text>
</View>
)}
{ this.renderMarking() }
</View>
</TouchableOpacity>
);
}
return calendarAnimation && !scrollable ? (
<Animated.View style={[
styles.dateRootContainer,
{opacity: this.state.animatedValue}
]}>
{day}
</Animated.View>
) : (
<View style={styles.dateRootContainer}>
{day}
</View>
);
}
}
export default CalendarDay;