calendar/calendar-react.js

330 lines
10 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* 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>);
}
}