Skip to content

Commit 7d27a91

Browse files
committed
Add datetime widget
1 parent fb10f13 commit 7d27a91

File tree

4 files changed

+305
-3
lines changed

4 files changed

+305
-3
lines changed

src/components/form.js

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Button from './buttons';
22
import Loader from './loaders';
3+
import {TimePicker} from './widgets';
34
import {EditorContext, getCsrfCookie, capitalize} from '../util';
45

56

@@ -361,3 +362,163 @@ export class FormTextareaInput extends React.Component {
361362
);
362363
}
363364
}
365+
366+
367+
export class FormDateTimeInput extends React.Component {
368+
constructor(props) {
369+
super(props);
370+
// we maintain this input's state in itself
371+
// so that we can only pass valid values
372+
// otherwise keep the value empty if invalid
373+
374+
let date = '';
375+
let hh = '12';
376+
let mm = '00';
377+
let ss = '00';
378+
let ms = '000';
379+
let ampm = 'am';
380+
381+
if (props.value) {
382+
let d = new Date(props.value);
383+
let year = d.getFullYear().toString().padStart(2, '0');
384+
let month = (d.getMonth() + 1).toString().padStart(2, '0');
385+
let day = d.getDate().toString().padStart(2, '0');
386+
date = year + '-' + month + '-' + day;
387+
388+
hh = d.getHours();
389+
if (hh === 0) {
390+
hh = 12;
391+
} else if (hh === 12) {
392+
ampm = 'pm';
393+
} else if (hh > 12) {
394+
hh = hh - 12;
395+
ampm = 'pm';
396+
}
397+
398+
mm = d.getMinutes();
399+
ss = d.getSeconds();
400+
ms = d.getMilliseconds();
401+
402+
hh = hh.toString().padStart(2, '0');
403+
mm = mm.toString().padStart(2, '0');
404+
ss = ss.toString().padStart(2, '0');
405+
}
406+
407+
this.state = {
408+
date: date,
409+
hh: hh,
410+
mm: mm,
411+
ss: ss,
412+
ms: ms,
413+
ampm: ampm,
414+
showTimePicker: false,
415+
};
416+
417+
this.timeInput = React.createRef();
418+
this.timePickerContainer = React.createRef();
419+
}
420+
421+
componentDidMount() {
422+
document.addEventListener('mousedown', this.handleClickOutside);
423+
}
424+
425+
componentWillUnmount() {
426+
document.removeEventListener('mousedown', this.handleClickOutside);
427+
}
428+
429+
handleClickOutside = (e) => {
430+
if (this.state.showTimePicker) {
431+
if (this.timePickerContainer.current &&
432+
!this.timePickerContainer.current.contains(e.target) &&
433+
!this.timeInput.current.contains(e.target)
434+
)
435+
this.setState({showTimePicker: false});
436+
}
437+
};
438+
439+
sendValue = () => {
440+
// we create a fake event object
441+
// to send a combined value from two inputs
442+
let event = {
443+
target: {
444+
type: 'text',
445+
value: '',
446+
name: this.props.name
447+
}
448+
};
449+
450+
if (this.state.date === '' || this.state.date === null)
451+
return this.props.onChange(event);
452+
453+
let hh = parseInt(this.state.hh);
454+
455+
if (this.state.ampm === 'am') {
456+
if (hh === 12)
457+
hh = 0;
458+
} else if (this.state.ampm === 'pm') {
459+
if (hh !== 12)
460+
hh = hh + 12;
461+
}
462+
463+
hh = hh.toString().padStart(2, '0');
464+
let mm = this.state.mm.padStart(2, '0');
465+
let ss = this.state.ss.padStart(2, '0');
466+
467+
let date = new Date(this.state.date + 'T' + hh + ':' + mm + ':' + ss + '.' + this.state.ms);
468+
let value = date.toISOString().replace('Z', '+00:00') // make compatible to python
469+
470+
event['target']['value'] = value;
471+
472+
this.props.onChange(event);
473+
}
474+
475+
handleDateChange = (e) => {
476+
this.setState({date: e.target.value}, this.sendValue);
477+
}
478+
479+
handleTimeChange = (value) => {
480+
this.setState({
481+
hh: value.hh,
482+
mm: value.mm,
483+
ss: value.ss,
484+
ampm: value.ampm,
485+
}, this.sendValue);
486+
}
487+
488+
showTimePicker = () => {
489+
this.setState({showTimePicker: true});
490+
}
491+
492+
render() {
493+
return (
494+
<div className="rjf-datetime-field">
495+
{this.props.label && <label>{this.props.label}</label>}
496+
<FormInput
497+
label='Date'
498+
type='date'
499+
value={this.state.date}
500+
onChange={this.handleDateChange}
501+
/>
502+
<FormInput
503+
label='Time'
504+
type='text'
505+
value={this.state.hh + ':' + this.state.mm + ':' + this.state.ss + ' ' + this.state.ampm}
506+
onFocus={this.showTimePicker}
507+
readOnly={true}
508+
inputRef={this.timeInput}
509+
/>
510+
<div ref={this.timePickerContainer}>
511+
{this.state.showTimePicker &&
512+
<TimePicker
513+
onChange={this.handleTimeChange}
514+
hh={this.state.hh}
515+
mm={this.state.mm}
516+
ss={this.state.ss}
517+
ampm={this.state.ampm}
518+
/>
519+
}
520+
</div>
521+
</div>
522+
);
523+
}
524+
}

src/components/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import Button from './buttons';
22
import {FormInput, FormCheckInput, FormRadioInput, FormSelectInput, FormFileInput,
3-
FormTextareaInput} from './form';
3+
FormTextareaInput, FormDateTimeInput} from './form';
44
import {FormRow, FormGroup, FormRowControls} from './containers';
55
import Loader from './loaders';
66
import Icon from './icons';
77

88
export {
99
Button,
1010
FormInput, FormCheckInput, FormRadioInput, FormSelectInput, FormFileInput,
11-
FormTextareaInput,
11+
FormTextareaInput, FormDateTimeInput,
1212
FormRow, FormGroup, FormRowControls,
1313
Loader,
1414
Icon,

src/components/widgets.js

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import Button from './buttons';
2+
import Icon from './icons';
3+
4+
5+
export class TimePicker extends React.Component {
6+
constructor(props) {
7+
super(props);
8+
9+
this.state = {
10+
hh: props.hh || '00',
11+
mm: props.mm || '00',
12+
ss: props.ss || '00',
13+
ampm: props.ampm || 'am',
14+
};
15+
}
16+
17+
componentDidUpdate(prevProps, prevState) {
18+
if (this.state !== prevState)
19+
this.props.onChange(this.state);
20+
}
21+
22+
validateValue = (name, value) => {
23+
if (name === 'hh' && value < 1)
24+
return 1;
25+
else if (name !== 'hh' && value < 0)
26+
return 0;
27+
else if (name === 'hh' && value > 12)
28+
return 12;
29+
else if (name !== 'hh' && value > 59)
30+
return 59;
31+
32+
return value;
33+
}
34+
35+
handleChange = (e) => {
36+
let name = e.target.name;
37+
let value = e.target.value;
38+
39+
if (isNaN(value))
40+
return;
41+
42+
let validValue = this.validateValue(name, parseInt(value) || 0);
43+
44+
if (value.startsWith('0') && validValue < 10 && validValue !== 0) {
45+
validValue = validValue.toString().padStart(2, '0');
46+
}
47+
48+
this.setState({[name]: value !== '' ? validValue.toString() : ''});
49+
}
50+
51+
handleKeyDown = (e) => {
52+
if (e.keyCode !== 38 && e.keyCode !== 40)
53+
return;
54+
55+
let name = e.target.name;
56+
let value = parseInt(e.target.value) || 0;
57+
58+
if (e.keyCode === 38) {
59+
value++;
60+
} else if (e.keyCode === 40) {
61+
value--;
62+
}
63+
64+
this.setState({[name]: this.validateValue(name, value).toString().padStart(2, '0')});
65+
}
66+
67+
handleSpin = (name, type) => {
68+
this.setState((state) => {
69+
let value = state[name];
70+
71+
if (name === 'ampm') {
72+
value = value === 'am' ? 'pm': 'am';
73+
} else {
74+
value = parseInt(value) || 0;
75+
if (type === 'up') {
76+
value++;
77+
} else {
78+
value--;
79+
}
80+
value = this.validateValue(name, value).toString().padStart(2, '0');
81+
}
82+
83+
return {[name]: value};
84+
});
85+
}
86+
87+
handleBlur = (e) => {
88+
if ((parseInt(e.target.value) || 0) < 10) {
89+
this.setState({[e.target.name]: e.target.value.padStart(2, '0')});
90+
}
91+
}
92+
93+
render() {
94+
return (
95+
<div className="rjf-time-picker">
96+
<div className="rjf-time-picker-row rjf-time-picker-labels">
97+
<div className="rjf-time-picker-col">Hrs</div>
98+
<div className="rjf-time-picker-col rjf-time-picker-col-sm"></div>
99+
<div className="rjf-time-picker-col">Min</div>
100+
<div className="rjf-time-picker-col rjf-time-picker-col-sm"></div>
101+
<div className="rjf-time-picker-col">Sec</div>
102+
<div className="rjf-time-picker-col rjf-time-picker-col-sm"></div>
103+
<div className="rjf-time-picker-col">am/pm</div>
104+
</div>
105+
106+
<div className="rjf-time-picker-row">
107+
<div className="rjf-time-picker-col"><Button onClick={() => this.handleSpin('hh', 'up')}><Icon name="chevron-up"/></Button></div>
108+
<div className="rjf-time-picker-col rjf-time-picker-col-sm"></div>
109+
<div className="rjf-time-picker-col"><Button onClick={() => this.handleSpin('mm', 'up')}><Icon name="chevron-up"/></Button></div>
110+
<div className="rjf-time-picker-col rjf-time-picker-col-sm"></div>
111+
<div className="rjf-time-picker-col"><Button onClick={() => this.handleSpin('ss', 'up')}><Icon name="chevron-up"/></Button></div>
112+
<div className="rjf-time-picker-col rjf-time-picker-col-sm"></div>
113+
<div className="rjf-time-picker-col"><Button onClick={() => this.handleSpin('ampm', 'up')}><Icon name="chevron-up"/></Button></div>
114+
</div>
115+
116+
<div className="rjf-time-picker-row rjf-time-picker-values">
117+
<div className="rjf-time-picker-col"><input type="text" name="hh" value={this.state.hh} onChange={this.handleChange} onBlur={this.handleBlur} onKeyDown={this.handleKeyDown} /></div>
118+
<div className="rjf-time-picker-col rjf-time-picker-col-sm">:</div>
119+
<div className="rjf-time-picker-col"><input type="text" name="mm" value={this.state.mm} onChange={this.handleChange} onBlur={this.handleBlur} onKeyDown={this.handleKeyDown} /></div>
120+
<div className="rjf-time-picker-col rjf-time-picker-col-sm">:</div>
121+
<div className="rjf-time-picker-col"><input type="text" name="ss" value={this.state.ss} onChange={this.handleChange} onBlur={this.handleBlur} onKeyDown={this.handleKeyDown} /></div>
122+
<div className="rjf-time-picker-col rjf-time-picker-col-sm"></div>
123+
<div className="rjf-time-picker-col">{this.state.ampm}</div>
124+
</div>
125+
126+
<div className="rjf-time-picker-row">
127+
<div className="rjf-time-picker-col"><Button onClick={() => this.handleSpin('hh', 'down')}><Icon name="chevron-down"/></Button></div>
128+
<div className="rjf-time-picker-col rjf-time-picker-col-sm"></div>
129+
<div className="rjf-time-picker-col"><Button onClick={() => this.handleSpin('mm', 'down')}><Icon name="chevron-down"/></Button></div>
130+
<div className="rjf-time-picker-col rjf-time-picker-col-sm"></div>
131+
<div className="rjf-time-picker-col"><Button onClick={() => this.handleSpin('ss', 'down')}><Icon name="chevron-down"/></Button></div>
132+
<div className="rjf-time-picker-col rjf-time-picker-col-sm"></div>
133+
<div className="rjf-time-picker-col"><Button onClick={() => this.handleSpin('ampm', 'down')}><Icon name="chevron-down"/></Button></div>
134+
</div>
135+
</div>
136+
);
137+
}
138+
}

src/ui.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {getBlankData} from './data';
22
import {Button, FormInput, FormCheckInput, FormRadioInput, FormSelectInput,
3-
FormFileInput, FormRow, FormGroup, FormRowControls, FormTextareaInput} from './components';
3+
FormFileInput, FormRow, FormGroup, FormRowControls, FormTextareaInput,
4+
FormDateTimeInput} from './components';
45
import {getVerboseName} from './util';
56

67

@@ -55,6 +56,8 @@ function FormField(props) {
5556
if (props.schema.format) {
5657
if (props.schema.format === 'data-url' || props.schema.format === 'file-url') {
5758
InputField = FormFileInput;
59+
} else if (props.schema.format === 'datetime') {
60+
InputField = FormDateTimeInput;
5861
}
5962
inputProps.type = props.schema.format;
6063
}

0 commit comments

Comments
 (0)