axiom-react-calendar
Version:
A component for picking dates or date periods for your React application.
424 lines (357 loc) • 12 kB
JSX
import React, { Component } from "react";
import PropTypes from "prop-types";
import mergeClassNames from "merge-class-names";
import Navigation from "./Calendar/Navigation";
import CenturyView from "./CenturyView";
import DecadeView from "./DecadeView";
import YearView from "./YearView";
import MonthView from "./MonthView";
import { getBegin, getEnd, getValueRange } from "./shared/dates";
import { setLocale } from "./shared/locales";
import { isCalendarType, isClassName, isMaxDate, isMinDate, isValue } from "./shared/propTypes";
import { between, callIfDefined, mergeFunctions } from "./shared/utils";
const allViews = ["century", "decade", "year", "month"];
const allValueTypes = [...allViews.slice(1), "day"];
const datesAreDifferent = (date1, date2) => (date1 && !date2) || (!date1 && date2) || (date1 && date2 && date1.getTime() !== date2.getTime());
export default class Calendar extends Component {
get drillDownAvailable() {
const views = this.getLimitedViews();
const { view } = this.state;
return views.indexOf(view) < views.length - 1;
}
get drillUpAvailable() {
const views = this.getLimitedViews();
const { view } = this.state;
return views.indexOf(view) > 0;
}
/**
* Returns value type that can be returned with currently applied settings.
*/
get valueType() {
const { maxDetail } = this.props;
return allValueTypes[allViews.indexOf(maxDetail)];
}
getValueArray(value) {
if (value instanceof Array) {
return value;
}
return [this.getValueFrom(value), this.getValueTo(value)];
}
getValueFrom = value => {
if (!value) {
return null;
}
const { maxDate, minDate } = this.props;
const rawValueFrom = value instanceof Array && value.length === 2 ? value[0] : value;
const valueFromDate = new Date(rawValueFrom);
if (Number.isNaN(valueFromDate.getTime())) {
throw new Error(`Invalid date: ${value}`);
}
const valueFrom = getBegin(this.valueType, valueFromDate);
return between(valueFrom, minDate, maxDate);
};
getValueTo = value => {
if (!value) {
return null;
}
const { maxDate, minDate } = this.props;
const rawValueTo = value instanceof Array && value.length === 2 ? value[1] : value;
const valueToDate = new Date(rawValueTo);
if (Number.isNaN(valueToDate.getTime())) {
throw new Error(`Invalid date: ${value}`);
}
const valueTo = getEnd(this.valueType, valueToDate);
return between(valueTo, minDate, maxDate);
};
/**
* Returns views array with disallowed values cut off.
*/
getLimitedViews(props = this.props) {
const { minDetail, maxDetail } = props;
return allViews.slice(allViews.indexOf(minDetail), allViews.indexOf(maxDetail) + 1);
}
/**
* Determines whether a given view is allowed with currently applied settings.
*/
isViewAllowed(props = this.props, view = this.state.view) {
const views = this.getLimitedViews(props);
return views.indexOf(view) !== -1;
}
/**
* Gets current value in a desired format.
*/
getProcessedValue(value) {
const { returnValue } = this.props;
switch (returnValue) {
case "start":
return this.getValueFrom(value);
case "end":
return this.getValueTo(value);
case "range":
return this.getValueArray(value);
default:
throw new Error("Invalid returnValue.");
}
}
state = {
activeStartDate: this.getActiveStartDate(),
hover: null,
view: this.getView(),
value: this.props.value
};
componentWillMount() {
setLocale(this.props.locale);
}
componentWillReceiveProps(nextProps) {
const { locale: nextLocale, value: nextValue } = nextProps;
const { locale } = this.props;
const { value } = this.state;
if (nextLocale !== locale) {
setLocale(nextLocale);
}
const nextState = {};
const allowedViewChanged = nextProps.minDetail !== this.props.minDetail || nextProps.maxDetail !== this.props.maxDetail;
if (allowedViewChanged && !this.isViewAllowed(nextProps)) {
nextState.view = this.getView(nextProps);
}
if (
allowedViewChanged ||
datesAreDifferent(...[nextValue, value].map(this.getValueFrom)) ||
datesAreDifferent(...[nextValue, value].map(this.getValueTo))
) {
this.updateValues(nextProps);
} else {
nextState.activeStartDate = this.getActiveStartDate(nextProps);
}
if (!nextProps.selectRange && this.props.selectRange) {
nextState.hover = null;
}
this.setState(nextState);
}
updateValues = (props = this.props) => {
this.setState({
value: props.value,
activeStartDate: this.getActiveStartDate(props)
});
};
getActiveStartDate(props = this.props) {
const rangeType = this.getView(props);
const valueFrom = this.getValueFrom(props.value) || props.activeStartDate || new Date();
return getBegin(rangeType, valueFrom);
}
getView(props = this.props) {
const { view } = props;
if (view && this.getLimitedViews(props).indexOf(view) !== -1) {
return view;
}
return this.getLimitedViews(props).pop();
}
/**
* Called when the user uses navigation buttons.
*/
setActiveStartDate = activeStartDate => {
this.setState({ activeStartDate }, () => {
callIfDefined(this.props.onActiveDateChange, {
activeStartDate,
view: this.state.view
});
});
};
drillDown = activeStartDate => {
if (!this.drillDownAvailable) {
return;
}
const views = this.getLimitedViews();
this.setState(
prevState => {
const nextView = views[views.indexOf(prevState.view) + 1];
return {
activeStartDate,
view: nextView
};
},
() => {
callIfDefined(this.props.onDrillDown, {
activeStartDate,
view: this.state.view
});
}
);
};
drillUp = () => {
if (!this.drillUpAvailable) {
return;
}
const views = this.getLimitedViews();
this.setState(
prevState => {
const nextView = views[views.indexOf(prevState.view) - 1];
const activeStartDate = getBegin(nextView, prevState.activeStartDate);
return {
activeStartDate,
view: nextView
};
},
() => {
callIfDefined(this.props.onDrillUp, {
activeStartDate: this.state.activeStartDate,
view: this.state.view
});
}
);
};
onChange = value => {
const { onChange, selectRange } = this.props;
let nextValue;
let callback;
if (selectRange) {
const { value: previousValue } = this.state;
// Range selection turned on
if (
!previousValue ||
[].concat(previousValue).length !== 1 // 0 or 2 - either way we're starting a new array
) {
// First value
nextValue = getBegin(this.valueType, value);
} else {
// Second value
nextValue = getValueRange(this.valueType, previousValue, value);
callback = () => callIfDefined(onChange, nextValue);
}
} else {
// Range selection turned off
nextValue = this.getProcessedValue(value);
callback = () => callIfDefined(onChange, nextValue);
}
this.setState({ value: nextValue }, callback);
};
onMouseOver = value => {
this.setState({ hover: value });
};
onMouseOut = () => {
this.setState({ hover: null });
};
renderContent() {
const { calendarType, maxDate, minDate, renderChildren, tileClassName, tileContent } = this.props;
const { activeStartDate, hover, value, view } = this.state;
const { onMouseOver, valueType } = this;
const commonProps = {
activeStartDate,
hover,
maxDate,
minDate,
onMouseOver: this.props.selectRange ? onMouseOver : null,
tileClassName,
tileContent: tileContent || renderChildren, // For backwards compatibility
value,
valueType
};
const clickAction = this.drillDownAvailable ? this.drillDown : this.onChange;
switch (view) {
case "century":
return <CenturyView onClick={mergeFunctions(clickAction, this.props.onClickDecade)} {...commonProps} />;
case "decade":
return <DecadeView onClick={mergeFunctions(clickAction, this.props.onClickYear)} {...commonProps} />;
case "year":
return <YearView formatMonth={this.props.formatMonth} onClick={mergeFunctions(clickAction, this.props.onClickMonth)} {...commonProps} />;
case "month":
return (
<MonthView
calendarType={calendarType}
formatShortWeekday={this.props.formatShortWeekday}
onClick={mergeFunctions(clickAction, this.props.onClickDay)}
onClickWeekNumber={this.props.onClickWeekNumber}
showNeighboringMonth={this.props.showNeighboringMonth}
showWeekNumbers={this.props.showWeekNumbers}
{...commonProps}
/>
);
default:
throw new Error(`Invalid view: ${view}.`);
}
}
renderNavigation() {
const { showNavigation } = this.props;
if (!showNavigation) {
return null;
}
return (
<Navigation
activeRange={this.state.activeRange}
activeStartDate={this.state.activeStartDate}
drillUp={this.drillUp}
formatMonthYear={this.props.formatMonthYear}
maxDate={this.props.maxDate}
minDate={this.props.minDate}
next2Label={this.props.next2Label}
nextLabel={this.props.nextLabel}
prev2Label={this.props.prev2Label}
prevLabel={this.props.prevLabel}
setActiveStartDate={this.setActiveStartDate}
view={this.state.view}
views={this.getLimitedViews()}
/>
);
}
render() {
console.log("hello-calendar");
const { className, selectRange } = this.props;
const { value } = this.state;
const { onMouseOut } = this;
const valueArray = [].concat(value);
return (
<div
className={mergeClassNames("react-calendar", selectRange && valueArray.length === 1 && "react-calendar--selectRange", className)}
onMouseOut={selectRange ? onMouseOut : null}
onBlur={selectRange ? onMouseOut : null}
>
{this.renderNavigation()}
{this.renderContent()}
</div>
);
}
}
Calendar.defaultProps = {
maxDetail: "month",
minDetail: "century",
returnValue: "start",
showNavigation: true,
showNeighboringMonth: true,
view: "month"
};
Calendar.propTypes = {
activeStartDate: PropTypes.instanceOf(Date),
calendarType: isCalendarType,
className: isClassName,
formatMonth: PropTypes.func,
formatMonthYear: PropTypes.func,
formatShortWeekday: PropTypes.func,
locale: PropTypes.string,
maxDate: isMaxDate,
maxDetail: PropTypes.oneOf(allViews),
minDate: isMinDate,
minDetail: PropTypes.oneOf(allViews),
next2Label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
nextLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
onActiveDateChange: PropTypes.func,
onChange: PropTypes.func,
onClickDay: PropTypes.func,
onClickDecade: PropTypes.func,
onClickMonth: PropTypes.func,
onClickWeekNumber: PropTypes.func,
onClickYear: PropTypes.func,
onDrillDown: PropTypes.func,
onDrillUp: PropTypes.func,
prev2Label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
prevLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
renderChildren: PropTypes.func, // For backwards compatibility
returnValue: PropTypes.oneOf(["start", "end", "range"]),
selectRange: PropTypes.bool,
showNavigation: PropTypes.bool,
showNeighboringMonth: PropTypes.bool,
showWeekNumbers: PropTypes.bool,
tileClassName: PropTypes.oneOfType([PropTypes.func, isClassName]),
tileContent: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
value: PropTypes.oneOfType([PropTypes.string, isValue]),
view: PropTypes.oneOf(allViews)
};