Another, React + Selectbox Picker based rewrite

master
Vitaliy Filippov 2021-09-19 21:33:35 +03:00
parent 8ea8f1511d
commit cf6f1bdc45
3 changed files with 415 additions and 30 deletions

75
CalendarInput.js Normal file
View File

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

View File

@ -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;
}

321
calendar-react.js Normal file
View File

@ -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'>&nbsp;</td>);
})}
</tr> : null)
))}
</tbody></table>);
}
}