Another, React + Selectbox Picker based rewrite
parent
8ea8f1511d
commit
cf6f1bdc45
|
@ -0,0 +1,75 @@
|
|||
// Input with calendar hint based on Calendar and Picker
|
||||
// Version 2021-09-19
|
||||
// License: LGPLv3.0+
|
||||
// (c) Vitaliy Filippov 2021+
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Calendar from './calendar-react.js';
|
||||
import Picker from './Picker.js';
|
||||
|
||||
export default class CalendarInput extends Picker
|
||||
{
|
||||
static propTypes = {
|
||||
value: PropTypes.oneOfType([ PropTypes.string, PropTypes.instanceOf(Date) ]),
|
||||
onChange: PropTypes.func.isRequired,
|
||||
className: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
closeLabel: PropTypes.string,
|
||||
monthNames: PropTypes.arrayOf(PropTypes.string),
|
||||
weekdays: PropTypes.arrayOf(PropTypes.string),
|
||||
weekdayIds: PropTypes.arrayOf(PropTypes.string),
|
||||
sunday: PropTypes.number,
|
||||
minDate: PropTypes.oneOfType([ PropTypes.oneOf(['today']), PropTypes.instanceOf(Date) ]),
|
||||
maxDate: PropTypes.oneOfType([ PropTypes.oneOf(['today']), PropTypes.instanceOf(Date) ]),
|
||||
format: PropTypes.oneOf([ 'dmy', 'ymd' ]),
|
||||
withTime: PropTypes.bool,
|
||||
startMode: PropTypes.oneOf([ 'days', 'months', 'years' ]),
|
||||
style: PropTypes.object,
|
||||
}
|
||||
|
||||
setText = (ev) => this.props.onChange(ev.target.value)
|
||||
|
||||
renderInput = (props) => (<input
|
||||
onBlur={props.onBlur}
|
||||
onFocus={props.onFocus}
|
||||
onClick={props.onFocus}
|
||||
ref={props.ref}
|
||||
style={this.props.style}
|
||||
className={this.props.className}
|
||||
type="text"
|
||||
value={this.props.value||''}
|
||||
onChange={this.setText}
|
||||
placeholder={this.props.placeholder}
|
||||
/>)
|
||||
|
||||
renderPicker = (props) => (<Calendar
|
||||
ref={this.animatePicker}
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
hide={props.blur}
|
||||
monthNames={this.props.monthNames}
|
||||
closeLabel={this.props.closeLabel}
|
||||
weekdays={this.props.weekdays}
|
||||
weekdayIds={this.props.weekdayIds}
|
||||
sunday={this.props.sunday}
|
||||
minDate={this.props.minDate}
|
||||
maxDate={this.props.maxDate}
|
||||
format={this.props.format}
|
||||
withTime={this.props.withTime}
|
||||
startMode={this.props.startMode}
|
||||
/>)
|
||||
|
||||
render()
|
||||
{
|
||||
return (<Picker
|
||||
direction={this.props.direction}
|
||||
minWidth={this.props.minWidth}
|
||||
autoHide={true}
|
||||
renderInput={this.renderInput}
|
||||
renderPicker={this.renderPicker}
|
||||
usePortal={true}
|
||||
/>);
|
||||
}
|
||||
}
|
|
@ -1,9 +1,7 @@
|
|||
/* Material Design CSS for calendar.js, version: 2018-03-14 */
|
||||
/* Material Design CSS for calendar-react, version: 2021-09-19 */
|
||||
.calendar-box {
|
||||
display: none;
|
||||
width: 301px;
|
||||
background: #fff;
|
||||
position: absolute;
|
||||
z-index: 10000;
|
||||
padding: 0;
|
||||
font-family: Roboto, sans-serif;
|
||||
|
@ -62,6 +60,9 @@
|
|||
cursor: pointer;
|
||||
transition: background 0.35s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||
}
|
||||
.calendar-box .clear {
|
||||
clear: right;
|
||||
}
|
||||
.calendar-box .calendar-cancel:hover {
|
||||
background: rgba(0, 0, 0, 0.14);
|
||||
}
|
||||
|
@ -111,15 +112,18 @@
|
|||
background: rgba(3, 169, 244, 0.3);
|
||||
}
|
||||
.calendar-box td.selected a {
|
||||
background: rgb(3, 169, 244);
|
||||
background: #03a9f4;
|
||||
color: white;
|
||||
}
|
||||
.calendar-box td.days.today a {
|
||||
border: 1px solid rgb(3, 169, 244);
|
||||
border: 1px solid #03a9f4;
|
||||
}
|
||||
.calendar-box td.days.today a:hover {
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.calendar-box td.disabled {
|
||||
background: #ddd;
|
||||
}
|
||||
.calendar-box tr.header td {
|
||||
color: #aaa;
|
||||
text-transform: uppercase;
|
||||
|
@ -128,8 +132,9 @@
|
|||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
}
|
||||
.calendar-box td { background: #f4f6f8; }
|
||||
.calendar-box td.future { color: #606060; }
|
||||
.calendar-box td {
|
||||
background: #f4f6f8;
|
||||
}
|
||||
.calendar-box .calendar-title {
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
|
@ -151,39 +156,23 @@
|
|||
transition: background 0.35s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||
}
|
||||
.calendar-box .calendar-title a:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
.calendar-box .calendar-title a.prev {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
padding: 0;
|
||||
width: 36px;
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='24px' height='24px' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z' style='fill:%23ffffff' /%3E%3C/svg%3E");
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.calendar-box .calendar-title a.next {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
padding: 0;
|
||||
width: 36px;
|
||||
}
|
||||
.calendar-box .calendar-title a.prev:after {
|
||||
content: "chevron_left";
|
||||
}
|
||||
.calendar-box .calendar-title a.next:after {
|
||||
content: "chevron_right";
|
||||
}
|
||||
.calendar-box .calendar-title a.prev:after,
|
||||
.calendar-box .calendar-title a.next:after {
|
||||
font-family: 'Material Icons';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
display: inline-block;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
direction: ltr;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-feature-settings: 'liga';
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='24px' height='24px' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z' style='fill:%23ffffff' /%3E%3C/svg%3E");
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,321 @@
|
|||
/**
|
||||
* 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
|
||||
*
|
||||
* Modified: http://yourcmc.ru/git/vitalif-js/calendar
|
||||
* Version: 2021-09-19
|
||||
* License: MIT
|
||||
*/
|
||||
|
||||
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()
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
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'> </td>);
|
||||
})}
|
||||
</tr> : null)
|
||||
))}
|
||||
</tbody></table>);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue