calendar/calendar-react.js

330 lines
10 KiB
JavaScript
Raw Permalink Normal View History

/**
* Calendar Script
* Creates a calendar widget which can be used to select the date
* Can be paired with Picker (https://yourcmc.ru/git/vitalif-js/selectbox/)
* to create a Calendar-based text input
*
* (c) Vitaliy Filippov 2011+
* Repository: http://yourcmc.ru/git/vitalif-js/calendar
* Version: 2021-10-17
* License: Dual-license MPL 2.0+ or GNU LGPL 3.0+
*/
import React from 'react';
export default class Calendar extends React.PureComponent
{
// Configuration
static defaultProps = {
monthNames: ["Январь","Февраль","Март","Апрель","Май","Июнь","Июль","Август","Сентябрь","Октябрь","Ноябрь","Декабрь"],
closeLabel: 'Закрыть',
weekdays: ["Пн","Вт","Ср","Чт","Пт","Сб","Вс"],
weekdayIds: ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'],
sunday: 6,
selectboxes: false, // true: use selectboxes for year and month, false: show months and years in table
minDate: null, // minimum date
maxDate: null, // maximum date
minYear: -70, // range of displayed years if selectboxes==true
maxYear: 10,
format: 'd.m.Y', // either d.m.Y or Y-m-d, other formats are not supported
time: false, // include time
startMode: 'days',
}
constructor(props)
{
super(props);
const selected = this.parseValue() || new Date();
this.state = {
mode: props.startMode || 'days',
year: selected.getFullYear(),
month: selected.getMonth(),
};
}
render()
{
return (<div className="calendar-box">
{this.state.mode == 'months' ? this.renderMonths() : null}
{this.state.mode == 'years' ? this.renderYears() : null}
{this.state.mode == 'days' ? this.renderDays() : null}
<a className="calendar-cancel" onClick={this.props.hide}>{this.props.closeLabel}</a>
<div className="clear" />
</div>);
}
/// Called when the user clicks on a date in the calendar.
selectDate(year, month, day)
{
let time = this.props.value;
if (!time)
time = [ 0, 0, 0 ];
else if (time instanceof Date)
time = [ time.getHours(), time.getMinutes(), time.getSeconds() ];
else
{
time = (''+time).split(/\s+/, 2)[1];
time = time ? time.split(/:/) : [ 0, 0, 0 ];
}
month = Number(month)+1;
if (!this.props.format)
{
// Safari does not understand new Date('YYYY-MM-DD HH:MM:SS')
time = new Date(year-0, month-1, day-0, time[0]-0, time[1]-0, time[2]-0);
}
else
{
if (month < 10)
month = '0'+month;
if (day < 10)
day = '0'+day;
time = time.map(t => t.length == 1 ? '0'+t : t).join(':');
time = (this.props.format == 'Y-m-d' ? year+'-'+month+'-'+day : day+'.'+month+'.'+year) +
(this.props.time ? ' '+time : '');
}
this.props.onChange(time);
this.props.hide && this.props.hide();
}
parseValue(update)
{
if (!this.prevProps || this.props.value != this.prevProps.value)
{
if (this.props.value instanceof Date)
this.selected = this.props.value;
else
{
this.selected = null;
let date_in_input = (''+this.props.value).replace(/\s+.*$/, ''); // Remove time
if (date_in_input)
{
// date format is HARDCODE
let date_parts = date_in_input.split("-");
if (date_parts.length == 3)
{
// Y-m-d
date_parts[1]--; // Month starts with 0
this.selected = new Date(date_parts[0], date_parts[1], date_parts[2]);
}
else if (date_parts.length == 1)
{
date_parts = date_in_input.split('.');
if (date_parts.length == 3)
{
// d.m.Y
date_parts[1]--; // Month starts with 0
this.selected = new Date(date_parts[2], date_parts[1], date_parts[0]);
}
}
if (isNaN(this.selected))
this.selected = null;
}
}
if (update)
{
setImmediate(() => this.setState({
year: this.selected.getFullYear(),
month: this.selected.getMonth(),
}));
}
this.prevProps = this.props;
}
return this.selected;
}
showMonths(year)
{
this.setState({ year, mode: 'months' });
}
showYears(year)
{
this.setState({ year, mode: 'years' });
}
showDays(year, month)
{
this.setState({ year, month, mode: 'days' });
}
renderMonths()
{
let year = this.state.year;
let today = this.props.today || new Date();
let cur_y = today.getFullYear();
let cur_m = today.getMonth();
let selected = this.parseValue(true);
let sel_m = selected && selected.getFullYear() == year ? selected.getMonth() : -1;
let months = [ [ 0, 1, 2 ], [ 3, 4, 5 ], [ 6, 7, 8 ], [ 9, 10, 11 ] ];
return (<table><tbody>
<tr><th colSpan='4' className='calendar-title'>
<a onClick={() => this.showMonths(year-1)} title={(year-1)} className='prev'></a>
<a onClick={() => this.showYears(year)}>{year}</a>
<a onClick={() => this.showMonths(year+1)} title={(year+1)} className='next'></a>
</th></tr>
{months.map((g, idx) => (<tr key={idx}>
{g.map(i => (
<td key={i} className={'months '+
(year < cur_y || year == cur_y && i < cur_m ? 'past' :
(year > cur_y || year == cur_y && i > cur_m ? 'future' : 'today'))
+ (i == sel_m ? ' selected' : '')}>
<a onClick={() => this.showDays(year, i)}>
{this.props.monthNames[i]}
</a>
</td>
))}
</tr>))}
</tbody></table>);
}
renderYears()
{
let year = this.state.year;
let beg = year & ~15;
let today = this.props.today || new Date();
let cur_y = today.getFullYear();
let selected = this.parseValue(true);
let sel_y = selected ? selected.getFullYear() : -1;
return (<table><tbody>
<tr><th colSpan='4' className='calendar-title'>
<a onClick={() => this.showYears(year-16)} title={(beg-16)+" - "+(beg-1)} className='prev'></a>
<b>{beg+' - '+(beg+15)}</b>
<a onClick={() => this.showYears(year+16)} title={(beg+16)+" - "+(beg+31)} className='next'></a>
</th></tr>
{[0, 1, 2, 3].map(r => (
<tr key={r}>
{[0, 1, 2, 3].map(j => {
let i = beg + j + r*4;
let class_name = (i < cur_y ? 'past' : (i > cur_y ? 'future' : 'today'))
+ (i == sel_y ? ' selected' : '');
return (<td key={j} className={'years '+class_name}>
<a onClick={() => this.showMonths(i)}>{i}</a>
</td>);
})}
</tr>
))}
</tbody></table>);
}
_yearOptions(min, max, year)
{
let r = [];
for (let i = min; i < max; i++)
r.push(<option value={i} selected={i == year}>{i}</option>);
return r;
}
/// Creates a calendar with the date given in the argument as the selected date.
renderDays()
{
let { year, month } = this.state;
let { selectboxes, sunday, monthNames } = this.props;
let selected = this.parseValue(true);
let today = this.props.today || new Date();
// Display the table
let next_month = month+1;
let next_month_year = year;
if (next_month >= 12)
{
next_month = 0;
next_month_year++;
}
let previous_month = month-1;
let previous_month_year = year;
if (previous_month < 0)
{
previous_month = 11;
previous_month_year--;
}
let current_year = today.getFullYear();
// Get the first day of this month
let first_day = new Date(year, month, 1);
let start_day = (first_day.getDay()+sunday)%7;
let d = 1;
let flag = 0;
// Leap year support
let days_in_this_month = (month == 2
? (!(year % 4) && ((year % 100) || !(year % 400)) ? 29 : 28)
: ((month < 7) == !(month & 1) ? 31 : 30));
const all_diff = (year - today.getFullYear()) || (month - today.getMonth());
const minDate = this.props.minDate === 'today' ? today : this.props.minDate;
const maxDate = this.props.maxDate === 'today' ? today : this.props.maxDate;
const month_disabled = minDate && (year < minDate.getFullYear() ||
year == minDate.getFullYear() && month < minDate.getMonth()) ||
maxDate && (year > maxDate.getFullYear() ||
year == maxDate.getFullYear() && month > maxDate.getMonth());
const min_md = minDate && year == minDate.getFullYear() &&
month == minDate.getMonth() ? minDate.getDate() : null;
const max_md = maxDate && year == maxDate.getFullYear() &&
month == maxDate.getMonth() ? maxDate.getDate() : null;
const sel_day = selected && year == selected.getFullYear() && month == selected.getMonth() ? selected.getDate() : -1;
return (<table><tbody>
<tr><th colSpan='7' className='calendar-title'>
<a onClick={() => this.showDays(previous_month_year, previous_month)}
title={monthNames[previous_month]+" "+previous_month_year} className='prev'></a>
{!selectboxes ?
[
<a key="1" onClick={() => this.showMonths(year, month)}>{monthNames[month]}</a>,
<a key="2" onClick={() => this.showYears(year)}>{year}</a>
] : [
<select name='calendar-month' className='calendar-month' onChange={(e) => this.showDays(year, e.target.value)}>
{monthNames.map((name, i) => (
<option value={i} selected={(i == month)}>{name}</option>
))}
</select>,
<select name='calendar-year' className='calendar-year' onChange={(e) => this.showDays(e.target.value, month)}>
{this._yearOptions(current_year+this.props.minYear, current_year+this.props.maxYear, year)}
</select>
]
}
<a onClick={() => this.showDays(next_month_year,next_month)}
title={this.props.monthNames[next_month]+" "+next_month_year} className='next'></a>
</th></tr>
<tr className='header'>
{this.props.weekdays.map((name, idx) => (<td key={idx}>{name}</td>))}
</tr>
{[0, 1, 2, 3, 4].map(i => (
(i*7 < days_in_this_month+start_day ? <tr key={i}>
{[0, 1, 2, 3, 4, 5, 6].map(j =>
{
let d = i*7+j+1-start_day;
let visible = (i > 0 || j >= start_day) && (d <= days_in_this_month);
if (visible)
{
let class_name = 'days';
let diff = all_diff || (d - today.getDate());
let disabled = month_disabled ||
min_md !== null && d < min_md ||
max_md !== null && d > max_md;
if (diff < 0)
class_name += ' past';
else if (!diff)
class_name += ' today';
else
class_name += ' future';
if (d == sel_day)
class_name += ' selected';
if (disabled)
class_name += ' disabled';
class_name += ' '+this.props.weekdayIds[j].toLowerCase();
return (<td key={j} className={class_name}>
<a onClick={disabled ? null : () => this.selectDate(year, month, d)}>{d}</a>
</td>);
}
else
return (<td key={j} className='days'>&nbsp;</td>);
})}
</tr> : null)
))}
</tbody></table>);
}
}