react-datetime
Version:
A lightweight but complete datetime picker React.js component.
413 lines (352 loc) • 10.3 kB
JavaScript
'use strict';
var assign = require('object-assign'),
React = require('react'),
DaysView = require('./src/DaysView'),
MonthsView = require('./src/MonthsView'),
YearsView = require('./src/YearsView'),
TimeView = require('./src/TimeView'),
moment = require('moment')
;
var TYPES = React.PropTypes;
var Datetime = React.createClass({
mixins: [
require('./src/onClickOutside')
],
viewComponents: {
days: DaysView,
months: MonthsView,
years: YearsView,
time: TimeView
},
propTypes: {
// value: TYPES.object | TYPES.string,
// defaultValue: TYPES.object | TYPES.string,
onFocus: TYPES.func,
onBlur: TYPES.func,
onChange: TYPES.func,
locale: TYPES.string,
utc: TYPES.bool,
input: TYPES.bool,
// dateFormat: TYPES.string | TYPES.bool,
// timeFormat: TYPES.string | TYPES.bool,
inputProps: TYPES.object,
timeConstraints: TYPES.object,
viewMode: TYPES.oneOf(['years', 'months', 'days', 'time']),
isValidDate: TYPES.func,
open: TYPES.bool,
strictParsing: TYPES.bool,
closeOnSelect: TYPES.bool,
closeOnTab: TYPES.bool
},
getDefaultProps: function() {
var nof = function(){};
return {
className: '',
defaultValue: '',
inputProps: {},
input: true,
onFocus: nof,
onBlur: nof,
onChange: nof,
timeFormat: true,
timeConstraints: {},
dateFormat: true,
strictParsing: true,
closeOnSelect: false,
closeOnTab: true,
utc: false
};
},
getInitialState: function() {
var state = this.getStateFromProps( this.props );
if ( state.open === undefined )
state.open = !this.props.input;
state.currentView = this.props.dateFormat ? (this.props.viewMode || state.updateOn || 'days') : 'time';
return state;
},
getStateFromProps: function( props ){
var formats = this.getFormats( props ),
date = props.value || props.defaultValue,
selectedDate, viewDate, updateOn, inputValue
;
if ( date && typeof date === 'string' )
selectedDate = this.localMoment( date, formats.datetime );
else if ( date )
selectedDate = this.localMoment( date );
if ( selectedDate && !selectedDate.isValid() )
selectedDate = null;
viewDate = selectedDate ?
selectedDate.clone().startOf('month') :
this.localMoment().startOf('month')
;
updateOn = this.getUpdateOn(formats);
if ( selectedDate )
inputValue = selectedDate.format(formats.datetime);
else if ( date.isValid && !date.isValid() )
inputValue = '';
else
inputValue = date || '';
return {
updateOn: updateOn,
inputFormat: formats.datetime,
viewDate: viewDate,
selectedDate: selectedDate,
inputValue: inputValue,
open: props.open
};
},
getUpdateOn: function(formats){
if ( formats.date.match(/[lLD]/) ){
return 'days';
}
else if ( formats.date.indexOf('M') !== -1 ){
return 'months';
}
else if ( formats.date.indexOf('Y') !== -1 ){
return 'years';
}
return 'days';
},
getFormats: function( props ){
var formats = {
date: props.dateFormat || '',
time: props.timeFormat || ''
},
locale = this.localMoment( props.date ).localeData()
;
if ( formats.date === true ){
formats.date = locale.longDateFormat('L');
}
else if ( this.getUpdateOn(formats) !== 'days' ){
formats.time = '';
}
if ( formats.time === true ){
formats.time = locale.longDateFormat('LT');
}
formats.datetime = formats.date && formats.time ?
formats.date + ' ' + formats.time :
formats.date || formats.time
;
return formats;
},
componentWillReceiveProps: function(nextProps) {
var formats = this.getFormats( nextProps ),
update = {}
;
if ( nextProps.value !== this.props.value ||
formats.datetime !== this.getFormats( this.props ).datetime ){
update = this.getStateFromProps( nextProps );
}
if ( update.open === undefined ){
if ( this.props.closeOnSelect && this.state.currentView !== 'time' ){
update.open = false;
}
else {
update.open = this.state.open;
}
}
this.setState( update );
},
onInputChange: function( e ) {
var value = e.target === null ? e : e.target.value,
localMoment = this.localMoment( value, this.state.inputFormat ),
update = { inputValue: value }
;
if ( localMoment.isValid() && !this.props.value ) {
update.selectedDate = localMoment;
update.viewDate = localMoment.clone().startOf('month');
}
else {
update.selectedDate = null;
}
return this.setState( update, function() {
return this.props.onChange( localMoment.isValid() ? localMoment : this.state.inputValue );
});
},
onInputKey: function( e ){
if ( e.which === 9 && this.props.closeOnTab ){
this.closeCalendar();
}
},
showView: function( view ){
var me = this;
return function(){
me.setState({ currentView: view });
};
},
setDate: function( type ){
var me = this,
nextViews = {
month: 'days',
year: 'months'
}
;
return function( e ){
me.setState({
viewDate: me.state.viewDate.clone()[ type ]( parseInt(e.target.getAttribute('data-value'), 10) ).startOf( type ),
currentView: nextViews[ type ]
});
};
},
addTime: function( amount, type, toSelected ){
return this.updateTime( 'add', amount, type, toSelected );
},
subtractTime: function( amount, type, toSelected ){
return this.updateTime( 'subtract', amount, type, toSelected );
},
updateTime: function( op, amount, type, toSelected ){
var me = this;
return function(){
var update = {},
date = toSelected ? 'selectedDate' : 'viewDate'
;
update[ date ] = me.state[ date ].clone()[ op ]( amount, type );
me.setState( update );
};
},
allowedSetTime: ['hours', 'minutes', 'seconds', 'milliseconds'],
setTime: function( type, value ){
var index = this.allowedSetTime.indexOf( type ) + 1,
state = this.state,
date = (state.selectedDate || state.viewDate).clone(),
nextType
;
// It is needed to set all the time properties
// to not to reset the time
date[ type ]( value );
for (; index < this.allowedSetTime.length; index++) {
nextType = this.allowedSetTime[index];
date[ nextType ]( date[nextType]() );
}
if ( !this.props.value ){
this.setState({
selectedDate: date,
inputValue: date.format( state.inputFormat )
});
}
this.props.onChange( date );
},
updateSelectedDate: function( e, close ) {
var target = e.target,
modifier = 0,
viewDate = this.state.viewDate,
currentDate = this.state.selectedDate || viewDate,
date
;
if (target.className.indexOf('rdtDay') !== -1){
if (target.className.indexOf('rdtNew') !== -1)
modifier = 1;
else if (target.className.indexOf('rdtOld') !== -1)
modifier = -1;
date = viewDate.clone()
.month( viewDate.month() + modifier )
.date( parseInt( target.getAttribute('data-value'), 10 ) );
} else if (target.className.indexOf('rdtMonth') !== -1){
date = viewDate.clone()
.month( parseInt( target.getAttribute('data-value'), 10 ) )
.date( currentDate.date() );
} else if (target.className.indexOf('rdtYear') !== -1){
date = viewDate.clone()
.month( currentDate.month() )
.date( currentDate.date() )
.year( parseInt( target.getAttribute('data-value'), 10 ) );
}
date.hours( currentDate.hours() )
.minutes( currentDate.minutes() )
.seconds( currentDate.seconds() )
.milliseconds( currentDate.milliseconds() );
if ( !this.props.value ){
this.setState({
selectedDate: date,
viewDate: date.clone().startOf('month'),
inputValue: date.format( this.state.inputFormat ),
open: !(this.props.closeOnSelect && close )
});
} else {
if (this.props.closeOnSelect && close) {
this.closeCalendar();
}
}
this.props.onChange( date );
},
openCalendar: function() {
if (!this.state.open) {
this.setState({ open: true }, function() {
this.props.onFocus();
});
}
},
closeCalendar: function() {
this.setState({ open: false }, function () {
this.props.onBlur( this.state.selectedDate || this.state.inputValue );
});
},
handleClickOutside: function(){
if ( this.props.input && this.state.open && !this.props.open ){
this.setState({ open: false }, function() {
this.props.onBlur( this.state.selectedDate || this.state.inputValue );
});
}
},
localMoment: function( date, format ){
var momentFn = this.props.utc ? moment.utc : moment;
var m = momentFn( date, format, this.props.strictParsing );
if ( this.props.locale )
m.locale( this.props.locale );
return m;
},
componentProps: {
fromProps: ['value', 'isValidDate', 'renderDay', 'renderMonth', 'renderYear', 'timeConstraints'],
fromState: ['viewDate', 'selectedDate', 'updateOn'],
fromThis: ['setDate', 'setTime', 'showView', 'addTime', 'subtractTime', 'updateSelectedDate', 'localMoment']
},
getComponentProps: function(){
var me = this,
formats = this.getFormats( this.props ),
props = {dateFormat: formats.date, timeFormat: formats.time}
;
this.componentProps.fromProps.forEach( function( name ){
props[ name ] = me.props[ name ];
});
this.componentProps.fromState.forEach( function( name ){
props[ name ] = me.state[ name ];
});
this.componentProps.fromThis.forEach( function( name ){
props[ name ] = me[ name ];
});
return props;
},
render: function() {
var Component = this.viewComponents[ this.state.currentView ],
DOM = React.DOM,
className = 'rdt' + (this.props.className ?
( Array.isArray( this.props.className ) ?
' ' + this.props.className.join( ' ' ) : ' ' + this.props.className) : ''),
children = []
;
if ( this.props.input ){
children = [ DOM.input( assign({
key: 'i',
type:'text',
className: 'form-control',
onFocus: this.openCalendar,
onChange: this.onInputChange,
onKeyDown: this.onInputKey,
value: this.state.inputValue
}, this.props.inputProps ))];
} else {
className += ' rdtStatic';
}
if ( this.state.open )
className += ' rdtOpen';
return DOM.div({className: className}, children.concat(
DOM.div(
{ key: 'dt', className: 'rdtPicker' },
React.createElement( Component, this.getComponentProps())
)
));
}
});
// Make moment accessible through the Datetime class
Datetime.moment = moment;
module.exports = Datetime;