Skip to content

Commit d788610

Browse files
author
Vladimir Spasic
committed
Added editable DateInput components to Calendar
1 parent a423741 commit d788610

File tree

6 files changed

+224
-61
lines changed

6 files changed

+224
-61
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ focusedRange(DateRange) | Object | | It defines
126126
onRangeFocusChange(DateRange) | Object | | Callback function for focus changes
127127
preview(DateRange) | Object | | displays a preview range and overwrite DateRange's default preview. Expected shape: `{ startDate: Date, endDate: Date, color: String }`
128128
showPreview(DateRange) | bool | true | visibility of preview
129+
editableDateInputs(Calendar) | bool | false | whether dates can be edited in the Calendar's input fields
129130
dragSelectionEnabled(Calendar) | bool | true | whether dates can be selected via drag n drop
130131
onPreviewChange(DateRange) | Object | | Callback function for preview changes
131132
dateDisplayFormat(DateRange) | String | `MMM D, YYYY` | selected range preview formatter. Check out [date-fns's format option](https://date-fns.org/v2.0.0-alpha.7/docs/format)

demo/src/components/Main.js

Lines changed: 63 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@ import * as rdrLocales from '../../../src/locale';
44
import { format, addDays } from 'date-fns';
55
import Section from './Section';
66

7-
function renderStaticRangeLabel(staticRange) {
8-
return (
9-
<CustomStaticRangeLabelContent text={'This is a custom label content: '}/>
10-
);
11-
}
7+
const renderStaticRangeLabel = () => (
8+
<CustomStaticRangeLabelContent text={'This is a custom label content: '} />
9+
);
1210

1311
class CustomStaticRangeLabelContent extends React.Component {
1412
constructor(props) {
@@ -18,14 +16,11 @@ class CustomStaticRangeLabelContent extends React.Component {
1816
currentDateString: Date(),
1917
};
2018

21-
this.intervalId = setInterval(
22-
() => {
23-
this.setState({
24-
currentDateString: Date(),
25-
});
26-
},
27-
1000
28-
);
19+
this.intervalId = setInterval(() => {
20+
this.setState({
21+
currentDateString: Date(),
22+
});
23+
}, 1000);
2924
}
3025

3126
componentWillUnmount() {
@@ -263,7 +258,7 @@ export default class Main extends Component {
263258
readOnly
264259
value={formatDateDisplay(this.state.multipleRanges.selection1.endDate, 'Continuous')}
265260
/>
266-
<div className={'newLine'}/>
261+
<div className={'newLine'} />
267262

268263
<label className={'label'}>Selection2 Start:</label>
269264
<input
@@ -277,7 +272,7 @@ export default class Main extends Component {
277272
readOnly
278273
value={formatDateDisplay(this.state.multipleRanges.selection2.endDate, 'Continuous')}
279274
/>
280-
<div className={'newLine'}/>
275+
<div className={'newLine'} />
281276

282277
<label className={'label'}>Selection3 Start:</label>
283278
<input
@@ -349,6 +344,35 @@ export default class Main extends Component {
349344
className={'PreviewArea'}
350345
/>
351346
</Section>
347+
348+
<Section title="RangePicker - Editable date fields">
349+
<div>
350+
<input
351+
type="text"
352+
readOnly
353+
value={formatDateDisplay(this.state.dateRange.selection.startDate)}
354+
/>
355+
<input
356+
type="text"
357+
readOnly
358+
value={formatDateDisplay(this.state.dateRange.selection.endDate, 'Continuous')}
359+
/>
360+
</div>
361+
<div>
362+
<p>
363+
You can edit the dates manually in the input fields. Be aware that the date value
364+
<b> must match the supplied date format.</b>
365+
</p>
366+
<DateRange
367+
onChange={this.handleRangeChange.bind(this, 'dateRange')}
368+
editableDateInputs={true}
369+
moveRangeOnFirstSelection={false}
370+
ranges={[this.state.dateRange.selection]}
371+
className={'PreviewArea'}
372+
/>
373+
</div>
374+
</Section>
375+
352376
<Section title="DefinedRange">
353377
<div>
354378
<input
@@ -366,23 +390,22 @@ export default class Main extends Component {
366390
<DefinedRange
367391
ranges={[this.state.definedRange.selection]}
368392
renderStaticRangeLabel={renderStaticRangeLabel}
369-
staticRanges={[{
370-
label: "Hoy",
371-
hasCustomRendering: true,
372-
range: () => ({
373-
startDate: new Date(),
374-
endDate: new Date(),
375-
}),
376-
isSelected() {
377-
return (
378-
true
379-
);
393+
staticRanges={[
394+
{
395+
label: 'Hoy',
396+
hasCustomRendering: true,
397+
range: () => ({
398+
startDate: new Date(),
399+
endDate: new Date(),
400+
}),
401+
isSelected() {
402+
return true;
403+
},
380404
},
381-
}]}
405+
]}
382406
onChange={this.handleRangeChange.bind(this, 'definedRange')}
383407
className={'centered'}
384-
>
385-
</DefinedRange>
408+
/>
386409
</Section>
387410
<Section title="RangePicker with disabled dates">
388411
<div>
@@ -413,18 +436,18 @@ export default class Main extends Component {
413436
<DefinedRange
414437
ranges={[this.state.definedRange.selection]}
415438
renderStaticRangeLabel={renderStaticRangeLabel}
416-
staticRanges={[{
417-
hasCustomRendering: true,
418-
range: () => ({
419-
startDate: new Date(),
420-
endDate: new Date(),
421-
}),
422-
isSelected() {
423-
return (
424-
true
425-
);
439+
staticRanges={[
440+
{
441+
hasCustomRendering: true,
442+
range: () => ({
443+
startDate: new Date(),
444+
endDate: new Date(),
445+
}),
446+
isSelected() {
447+
return true;
448+
},
426449
},
427-
}]}
450+
]}
428451
onChange={this.handleRangeChange.bind(this, 'definedRange')}
429452
className={'centered'}
430453
/>

src/components/Calendar.js

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { PureComponent } from 'react';
22
import PropTypes from 'prop-types';
33
import { rangeShape } from './DayCell.js';
44
import Month from './Month.js';
5+
import DateInput from './DateInput';
56
import { calcFocusDate, generateStyles, getMonthDisplayRange } from '../utils';
67
import classnames from 'classnames';
78
import ReactList from 'react-list';
@@ -238,9 +239,18 @@ class Calendar extends PureComponent {
238239
);
239240
}
240241
renderDateDisplay() {
241-
const { focusedRange, color, ranges, rangeColors } = this.props;
242+
const {
243+
focusedRange,
244+
color,
245+
ranges,
246+
rangeColors,
247+
dateDisplayFormat,
248+
editableDateInputs,
249+
} = this.props;
250+
242251
const defaultColor = rangeColors[focusedRange[0]] || color;
243252
const styles = this.styles;
253+
244254
return (
245255
<div className={styles.dateDisplayWrapper}>
246256
{ranges.map((range, i) => {
@@ -251,28 +261,32 @@ class Calendar extends PureComponent {
251261
className={styles.dateDisplay}
252262
key={i}
253263
style={{ color: range.color || defaultColor }}>
254-
<span
264+
<DateInput
255265
className={classnames(styles.dateDisplayItem, {
256266
[styles.dateDisplayItemActive]: focusedRange[0] === i && focusedRange[1] === 0,
257267
})}
258-
onFocus={() => this.handleRangeFocusChange(i, 0)}>
259-
<input
260-
disabled={range.disabled}
261-
readOnly
262-
value={this.formatDateDisplay(range.startDate, 'Early')}
263-
/>
264-
</span>
265-
<span
268+
readOnly={!editableDateInputs}
269+
disabled={range.disabled}
270+
value={range.startDate}
271+
placeholder="Early"
272+
dateOptions={this.dateOptions}
273+
dateDisplayFormat={dateDisplayFormat}
274+
onChange={this.onDragSelectionEnd}
275+
onFocus={() => this.handleRangeFocusChange(i, 0)}
276+
/>
277+
<DateInput
266278
className={classnames(styles.dateDisplayItem, {
267279
[styles.dateDisplayItemActive]: focusedRange[0] === i && focusedRange[1] === 1,
268280
})}
269-
onFocus={() => this.handleRangeFocusChange(i, 1)}>
270-
<input
271-
disabled={range.disabled}
272-
readOnly
273-
value={this.formatDateDisplay(range.endDate, 'Continuous')}
274-
/>
275-
</span>
281+
readOnly={!editableDateInputs}
282+
disabled={range.disabled}
283+
value={range.endDate}
284+
placeholder="Continuous"
285+
dateOptions={this.dateOptions}
286+
dateDisplayFormat={dateDisplayFormat}
287+
onChange={this.onDragSelectionEnd}
288+
onFocus={() => this.handleRangeFocusChange(i, 1)}
289+
/>
276290
</div>
277291
);
278292
})}
@@ -341,10 +355,6 @@ class Calendar extends PureComponent {
341355
const isLongMonth = differenceInDays(end, start, this.dateOptions) + 1 > 7 * 5;
342356
return isLongMonth ? scrollArea.longMonthHeight : scrollArea.monthHeight;
343357
}
344-
formatDateDisplay(date, defaultText) {
345-
if (!date) return defaultText;
346-
return format(date, this.props.dateDisplayFormat, this.dateOptions);
347-
}
348358
render() {
349359
const {
350360
showDateDisplay,
@@ -488,6 +498,7 @@ Calendar.defaultProps = {
488498
maxDate: addYears(new Date(), 20),
489499
minDate: addYears(new Date(), -100),
490500
rangeColors: ['#3d91ff', '#3ecf8e', '#fed14c'],
501+
editableDateInputs: false,
491502
dragSelectionEnabled: true,
492503
};
493504

@@ -533,6 +544,7 @@ Calendar.propTypes = {
533544
direction: PropTypes.oneOf(['vertical', 'horizontal']),
534545
navigatorRenderer: PropTypes.func,
535546
rangeColors: PropTypes.arrayOf(PropTypes.string),
547+
editableDateInputs: PropTypes.bool,
536548
dragSelectionEnabled: PropTypes.bool,
537549
};
538550

src/components/DateInput.js

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import React, { PureComponent } from 'react';
2+
import PropTypes from 'prop-types';
3+
import classnames from 'classnames';
4+
import { format, parse, isValid, isEqual } from 'date-fns';
5+
6+
class DateInput extends PureComponent {
7+
constructor(props, context) {
8+
super(props, context);
9+
10+
this.onKeyDown = this.onKeyDown.bind(this);
11+
this.onChange = this.onChange.bind(this);
12+
this.onBlur = this.onBlur.bind(this);
13+
14+
this.state = {
15+
invalid: false,
16+
changed: false,
17+
value: this.formatDate(props),
18+
};
19+
}
20+
21+
componentWillReceiveProps(nextProps) {
22+
const { value } = this.props;
23+
24+
if (!isEqual(value, nextProps.value)) {
25+
this.setState({ value: this.formatDate(nextProps) });
26+
}
27+
}
28+
29+
formatDate({ value, dateDisplayFormat, dateOptions }) {
30+
if (value && isValid(value)) {
31+
return format(value, dateDisplayFormat, dateOptions);
32+
}
33+
return '';
34+
}
35+
36+
update(value) {
37+
const { invalid, changed } = this.state;
38+
39+
if (invalid || !changed || !value) {
40+
return;
41+
}
42+
43+
const { onChange, dateDisplayFormat, dateOptions } = this.props;
44+
const parsed = parse(value, dateDisplayFormat, new Date(), dateOptions);
45+
46+
if (isValid(parsed)) {
47+
this.setState({ changed: false }, () => onChange(parsed));
48+
} else {
49+
this.setState({ invalid: true });
50+
}
51+
}
52+
53+
onKeyDown(e) {
54+
const { value } = this.state;
55+
56+
if (e.key === 'Enter') {
57+
this.update(value);
58+
}
59+
}
60+
61+
onChange(e) {
62+
this.setState({ value: e.target.value, changed: true, invalid: false });
63+
}
64+
65+
onBlur() {
66+
const { value } = this.state;
67+
this.update(value);
68+
}
69+
70+
render() {
71+
const { className, readOnly, placeholder, disabled, onFocus } = this.props;
72+
const { value, invalid } = this.state;
73+
74+
return (
75+
<span className={classnames('rdrDateInput', className)}>
76+
<input
77+
readOnly={readOnly}
78+
disabled={disabled}
79+
value={value}
80+
placeholder={placeholder}
81+
onKeyDown={this.onKeyDown}
82+
onChange={this.onChange}
83+
onBlur={this.onBlur}
84+
onFocus={onFocus}
85+
/>
86+
{invalid && <span className="rdrWarning">&#9888;</span>}
87+
</span>
88+
);
89+
}
90+
}
91+
92+
DateInput.propTypes = {
93+
value: PropTypes.object,
94+
placeholder: PropTypes.string,
95+
disabled: PropTypes.bool,
96+
readOnly: PropTypes.bool,
97+
dateOptions: PropTypes.object,
98+
dateDisplayFormat: PropTypes.string,
99+
className: PropTypes.string,
100+
onFocus: PropTypes.func.isRequired,
101+
onChange: PropTypes.func.isRequired,
102+
};
103+
104+
DateInput.defaultProps = {
105+
readOnly: true,
106+
disabled: false,
107+
dateDisplayFormat: 'MMM D, YYYY',
108+
};
109+
110+
export default DateInput;

src/components/dateInput.scss

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
.rdrDateInput {
2+
position: relative;
3+
4+
input {
5+
outline: none;
6+
}
7+
8+
.rdrWarning {
9+
position: absolute;
10+
font-size: 1.6em;
11+
line-height: 1.6em;
12+
top: 0;
13+
right: .25em;
14+
color: #FF0000;
15+
}
16+
}

0 commit comments

Comments
 (0)