react-pure-datepicker
Version:
563 lines (505 loc) • 16.9 kB
JavaScript
/* @flow */
import React, { Component } from 'react';
import ReactPureModal from 'react-pure-modal';
import instadate from 'instadate';
import { format as pureDateFormat } from 'react-pure-time';
import type {
Props,
State,
Accuracy,
Direction,
RenderedDate,
MouseClick,
Value,
Min,
Max,
Today,
WeekDaysNames,
} from './types.js';
class PureDatepicker extends Component<Props, State> {
static defaultProps = PureDatepicker.defaultProps;
static isDateValid(possibleDate: Date): boolean {
if (Object.prototype.toString.call(possibleDate) === '[object Date]') {
if (isNaN(possibleDate.getTime())) {
return false;
}
return true;
}
return false;
}
static getYearsPeriod(currentYear: number, years: number[]): number[] {
const period = [];
let fromYear = currentYear + years[0];
const toYear = currentYear + years[1];
while (fromYear <= toYear) {
period.push(fromYear);
fromYear += 1;
}
return period;
}
static normalizeDate(date: Date, accuracy: Accuracy = '', direction: Direction = ''): Date {
switch (`${accuracy}-${direction}`) {
case 'month-up':
return instadate.lastDateInMonth(date);
case 'month-down':
return instadate.firstDateInMonth(date);
case 'year-up':
return new Date(date.getFullYear(), 11, 31, 0, 0, 0, 0);
case 'year-down':
return new Date(date.getFullYear(), 0, 1, 0, 0, 0, 0);
default:
return new Date(date);
}
}
static toDate(dateString): ?Date {
const date = instadate.parseISOString(dateString);
if (this.isDateValid(date)) {
return instadate.noon(date);
}
return undefined;
}
constructor(props: Props) {
super(props);
this.getDateClasses = this.getDateClasses.bind(this);
this.getMonthClasses = this.getMonthClasses.bind(this);
this.getYearClasses = this.getYearClasses.bind(this);
this.handleClick = this.handleClick.bind(this);
this.hanleApplyBtnClick = this.hanleApplyBtnClick.bind(this);
this.openDatepickerModal = this.openDatepickerModal.bind(this);
this.handleCloseDatepickerModal = this.handleCloseDatepickerModal.bind(this);
this.isInRange = this.isInRange.bind(this);
this.clear = this.clear.bind(this);
this.getDaysNames = this.getDaysNames.bind(this);
this.getComponentState = this.getComponentState.bind(this);
this.state = this.getComponentState({}, this.props);
}
componentWillReceiveProps(nextProps: Props) {
this.setState(this.getComponentState(this.props, nextProps));
}
getComponentState: Function;
getComponentState(props: Props, nextProps: Props): State {
const updatedState = {};
const { min, max, value, today, calendarValue } = nextProps;
if (props.value !== value) {
updatedState.value = this.constructor.toDate(value);
}
if (props.calendarValue !== calendarValue) {
updatedState.calendarValue = this.constructor.toDate(calendarValue);
}
if (props.today !== today) {
updatedState.today = this.constructor.toDate(today);
}
if (props.min !== min) {
updatedState.min = this.constructor.toDate(min);
}
if (props.max !== max) {
updatedState.max = this.constructor.toDate(max);
}
updatedState.calendarValue = nextProps.applyBtn
? updatedState.calendarValue || updatedState.value // for init
: updatedState.value;
if (Object.keys(updatedState).length > 0) {
if (updatedState.min || updatedState.max) {
const currentDate = updatedState.calendarValue || props.calendarValue || updatedState.today || props.today;
if (!this.isInRange(currentDate, 'date', updatedState.min || false, updatedState.max || false)) {
if (updatedState.min && !updatedState.max) {
updatedState.calendarValue = updatedState.min;
} else if (updatedState.max && !updatedState.min) {
updatedState.calendarValue = updatedState.max;
} else if (updatedState.min && updatedState.max) {
if (instadate.isSameDay(updatedState.min, updatedState.max)) {
console.warn('Incorrect min and max. There no dates to choose!');
} else {
updatedState.calendarValue = updatedState.min;
}
}
}
}
}
return updatedState;
}
getDaysNames: Function;
getDaysNames(): WeekDaysNames | string[] {
if (this.props.beginFromDay < 7 && this.props.beginFromDay > -1) {
const firstPart = this.props.weekDaysNamesShort.slice(0, this.props.beginFromDay);
return this.props.weekDaysNamesShort
.slice(this.props.beginFromDay)
.concat(firstPart);
}
return this.props.weekDaysNamesShort;
}
getDateClasses: Function;
getDateClasses(date: Date, value: Value, renderedDate: RenderedDate): string {
const classes = ['date-cell'];
if (instadate.isWeekendDate(date)) classes.push('weekend');
if (!instadate.isSameMonth(date, renderedDate)) classes.push('out-month');
if (value) {
if (instadate.isSameDay(date, value)) classes.push('selected');
} else if (instadate.isSameDay(date, renderedDate)) {
classes.push('pre-selected');
}
if (!this.isInRange(date)) classes.push('out-min-max');
return classes.join(' ');
}
getMonthClasses: Function;
getMonthClasses(monthName: string, value: Value, renderedDate: RenderedDate): string {
const classes = ['monthName'];
const classForSameMonth = value ? 'selected' : 'pre-selected';
const date = value || renderedDate;
const dateByMonth = new Date(
date.getFullYear(),
this.props.monthsNames.indexOf(monthName),
1,
0,
0,
0,
0,
);
if (instadate.isSameMonth(dateByMonth, date)) classes.push(classForSameMonth);
if (!this.isInRange(dateByMonth, 'month')) classes.push('out-min-max');
return classes.join(' ');
}
getYearClasses: Function;
getYearClasses(year: number, value: Value, renderedDate: RenderedDate): string {
const classes = ['yearName'];
const classForSameYear = value ? 'selected' : 'pre-selected';
const date = value || renderedDate;
const dateByYear = new Date(year, 0, 1, 0, 0, 0, 0);
if (instadate.isSameYear(dateByYear, date)) classes.push(classForSameYear);
if (!this.isInRange(dateByYear, 'year')) classes.push('out-min-max');
return classes.join(' ');
}
isInRange: Function;
isInRange(date: Date, accuracy: Accuracy, min: Min = this.state.min, max: Max = this.state.max): boolean {
const normDateMin = this.constructor.normalizeDate(date, accuracy, 'up');
const normDateMax = this.constructor.normalizeDate(date, accuracy, 'down');
const minOk = min ? instadate.isAfter(normDateMin, instadate.addDays(min, -1)) : true;
const maxOk = max ? instadate.isBefore(normDateMax, instadate.addDays(max, 1)) : true;
return minOk && maxOk;
}
hanleApplyBtnClick: Function;
hanleApplyBtnClick() {
if (this.props.onChange) {
this.props.onChange(
pureDateFormat(this.state.calendarValue, this.props.returnFormat),
this.props.name,
);
return this.closeDatepickerModal();
}
}
handleClick: Function;
handleClick(e: MouseClick): void {
const { year, month, day } = e.currentTarget.dataset;
let accuracy;
let nextValue;
if (this.state.calendarValue) {
nextValue = new Date(this.state.calendarValue);
} else if (this.state.today) {
nextValue = new Date(this.state.today);
} else {
console.warn('Invalid Date value is choosen!');
return;
}
nextValue = instadate.noon(instadate.resetTimezoneOffset(nextValue));
if (year) {
accuracy = 'year';
nextValue.setFullYear(year);
}
if (month) {
nextValue.setMonth(month);
accuracy = 'month';
}
if (day) {
accuracy = 'date';
nextValue.setDate(day);
}
const inRange = this.isInRange(nextValue);
const inAccuracyRange = this.isInRange(nextValue, accuracy);
if (inRange || inAccuracyRange) {
if (inAccuracyRange) {
if (this.state.min && instadate.isBefore(nextValue, this.state.min)) {
nextValue = this.state.min;
}
if (this.state.max && instadate.isAfter(nextValue, this.state.max)) {
nextValue = this.state.max;
}
}
this.setState(
this.getComponentState(
this.props,
{ ...this.props, calendarValue: nextValue },
),
);
if (this.props.onChange && !this.props.applyBtn) {
this.props.onChange(
pureDateFormat(nextValue, this.props.returnFormat),
this.props.name,
);
if (accuracy === 'date') {
return this.closeDatepickerModal();
}
}
}
}
clear: Function;
clear(): void {
this.props.onChange('', this.props.name);
}
handleInput() {
}
openDatepickerModal: Function;
openDatepickerModal(e: MouseClick): void {
e.currentTarget.blur();
if (this.props.onFocus) {
this.props.onFocus(e);
}
this.refs.datepicker.open();
}
closeDatepickerModal() {
this.refs.datepicker.close();
}
handleCloseDatepickerModal: Function;
handleCloseDatepickerModal() {
this.setState({ calendarValue: this.state.value });
return true;
}
render() {
const {
format,
weekDaysNamesShort,
monthsNames,
years,
className,
placeholder,
inputClassName,
required,
onFocus,
disabled,
applyBtn,
beginFromDay,
...modalAttrs
} = this.props;
const { value, calendarValue, today } = this.state;
const renderedDate = calendarValue || today;
if (!renderedDate) {
console.warn('Invalid Date value is choosen!');
return null;
}
const weekDaysNames = this.getDaysNames();
const weekendsRef = weekDaysNamesShort.reduce((acc, dayName, i) => {
acc[dayName] = i === 0 || i === 6;
return acc;
}, {});
const firstDateInPeriod = instadate.firstDateInMonth(renderedDate);
const lastDateInPeriod = instadate.lastDateInMonth(renderedDate);
const datesShift = !(beginFromDay < 7 && beginFromDay > -1) ? 7 : beginFromDay;
const prevMonthDays = firstDateInPeriod.getDay() + (7 - datesShift);
const nextMonthDays = lastDateInPeriod.getDay() + (7 - datesShift);
const dates = instadate.dates(
instadate.addDays(firstDateInPeriod, -(prevMonthDays % 7)),
instadate.addDays(lastDateInPeriod, 6 - (nextMonthDays % 7)),
);
const centralYearInPeriod = renderedDate.getFullYear();
const yearsRange = this.constructor.getYearsPeriod(centralYearInPeriod, years);
const isTodayInRange = this.isInRange(this.state.today);
return (
<div className={className}>
<input
type="text"
onFocus={this.openDatepickerModal}
disabled={disabled}
className={inputClassName}
placeholder={placeholder}
required={required}
onChange={this.handleInput}
value={value ? pureDateFormat(this.state.value, format) : ''}
/>
<ReactPureModal
width="500px"
header="Select date"
ref="datepicker"
className="react-pure-calendar-modal"
onClose={this.handleCloseDatepickerModal}
{...modalAttrs}
>
<div className="react-pure-calendar">
<div className="calendarWrap">
<div className="weekdays-names">
{
weekDaysNames.map(weekDayName => (
<div
key={weekDayName}
className={`${weekendsRef[weekDayName] ? 'weekend' : ''} weekDayNameShort`}
>{weekDayName}</div>
))
}
</div>
{
dates.map((dateObject) => {
const date = dateObject.getDate();
const month = dateObject.getMonth();
const year = dateObject.getFullYear();
return (
<div
key={`${month}-${date}`}
className={this.getDateClasses(
dateObject,
this.state.calendarValue,
renderedDate,
)}
data-day-cell
data-day={date}
data-month={month}
data-year={year}
onClick={this.handleClick}
>{date}</div>
);
})
}
<br />
<br />
<button
onClick={this.handleClick}
type="button"
data-btn-today
data-day={this.state.today ? this.state.today.getDate() : false}
data-month={this.state.today ? this.state.today.getMonth() : false}
data-year={this.state.today ? this.state.today.getFullYear() : false}
className="btn btn-block btn-sm btn-default"
disabled={!isTodayInRange}
title={!isTodayInRange ? 'Today date is out of range' : ''}
>Today</button>
{
this.props.applyBtn ? (
<button
type="button"
data-btn-apply
className="btn btn-block btn-sm btn-default"
onClick={this.hanleApplyBtnClick}
disabled={!this.state.calendarValue}
>
Apply
</button>
) : null
}
{
this.props.clearBtn ? (
<button
data-btn-clear
onClick={this.clear}
type="button"
className="btn btn-block btn-sm btn-default"
>
Clear
</button>
) : null
}
</div>
<div data-month-section>
{
monthsNames.map((monthName, index) => (
<div
key={monthName}
data-month={index}
onClick={this.handleClick}
className={this.getMonthClasses(
monthName,
this.state.calendarValue,
renderedDate,
)}
>{monthName}</div>
))
}
</div>
<div data-year-section>
<div
data-arrow-smaller
data-year={yearsRange[0] + years[0]}
onClick={this.handleClick}
className={this.getYearClasses(
yearsRange[0] + years[0],
this.state.calendarValue,
renderedDate,
)}
>↑</div>
{
yearsRange.map(year => (
<div
key={year}
data-year={year}
onClick={this.handleClick}
className={this.getYearClasses(
year,
this.state.calendarValue,
renderedDate,
)}
>{year}</div>
))
}
<div
data-arrow-bigger
data-year={yearsRange[yearsRange.length - 1] + years[1]}
onClick={this.handleClick}
className={this.getYearClasses(
yearsRange[yearsRange.length - 1] + years[1],
this.state.calendarValue,
renderedDate,
)}
>↓</div>
</div>
</div>
</ReactPureModal>
</div>
);
}
}
PureDatepicker.defaultProps = {
today: instadate.noon(new Date()),
returnFormat: 'Y-m-d H:i:s',
format: 'd.m.Y',
disabled: false,
required: false,
monthsNames: [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
],
monthsNamesShort: [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
],
weekDaysNames: [
'Sunday',
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
],
weekDaysNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
years: [-4, 5],
beginFromDay: 0,
clearBtn: true,
applyBtn: false,
};
export default PureDatepicker;