react-widgets
Version:
354 lines (288 loc) • 10.7 kB
JavaScript
/** React.DOM */
var React = require('react')
, cx = require('../util/cx')
, _ = require('lodash')
, dates = require('../util/dates')
, mergeIntoProps = require('../util/transferProps').mergeIntoProps
, Popup = require('../popup/popup')
, Calendar = require('../calendar/calendar')
, Time = require('./time')
, DateInput = require('./date-input')
, $ = require('../util/dom');
var propTypes = {
value: React.PropTypes.instanceOf(Date),
onChange: React.PropTypes.func,
min: React.PropTypes.instanceOf(Date),
max: React.PropTypes.instanceOf(Date),
culture: React.PropTypes.string,
format: React.PropTypes.string,
editFormat: React.PropTypes.string,
calendar: React.PropTypes.bool,
time: React.PropTypes.bool,
timeComponent: React.PropTypes.func,
duration: React.PropTypes.number, //popup
placeholder: React.PropTypes.string,
initialView: React.PropTypes.oneOf(['month', 'year', 'decade', 'century']),
finalView: React.PropTypes.oneOf(['month', 'year', 'decade', 'century']),
disabled: React.PropTypes.oneOfType([
React.PropTypes.bool,
React.PropTypes.oneOf(['disabled'])
]),
readOnly: React.PropTypes.oneOfType([
React.PropTypes.bool,
React.PropTypes.oneOf(['readOnly'])
]),
parse: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.string),
React.PropTypes.string,
React.PropTypes.func
]),
}
module.exports = React.createClass({
displayName: 'DateTimePicker',
mixins: [
require('../mixins/PureRenderMixin'),
require('../mixins/RtlParentContextMixin')
],
propTypes: propTypes,
getInitialState: function(){
return {
selectedIndex: 0,
open: false,
openPopup: null
}
},
getDefaultProps: function(){
var cal = _.has(this.props, 'calendar') ? this.props.calendar : true
, time = _.has(this.props, 'time') ? this.props.time : true
, both = cal && time
, neither = !cal && !time;
return {
value: null,
format: both || neither
? 'M/d/yyyy h:mm tt'
: cal ? 'M/d/yyyy' : 'h:mm tt',
min: new Date(1900, 0, 1),
max: new Date(2099, 11, 31),
calendar: true,
time: true,
messages: {
calendarButton: 'Select Date',
timeButton: 'Select Time',
next: 'Next Date',
}
}
},
render: function(){
var self = this
, timeListID = this._id('_time_listbox')
, timeOptID = this._id('_time_option')
, dateListID = this._id('_cal')
, owns;
if (dateListID && this.props.calendar ) owns = dateListID
if (timeListID && this.props.time ) owns += ' ' + timeListID
return mergeIntoProps(
_.omit(this.props, _.keys(propTypes)),
React.DOM.div({ref: "element",
tabIndex: "-1",
onKeyDown: this._maybeHandle(this._keyDown),
onFocus: this._maybeHandle(_.partial(this._focus, true), true),
onBlur: _.partial(this._focus, false),
className: cx({
'rw-date-picker': true,
'rw-widget': true,
'rw-open': this.state.open,
'rw-state-focus': this.state.focused,
'rw-state-disabled': this.props.disabled,
'rw-state-readonly': this.props.readOnly,
'rw-has-both': this.props.calendar && this.props.time,
'rw-rtl': this.isRtl()
})},
DateInput({ref: "valueInput",
'aria-activedescendant': this.state.open
? this.state.openPopup === 'calendar' ? this._id('_cal_view_selected_item') : timeOptID
: undefined,
'aria-expanded': this.state.open,
'aria-busy': !!this.props.busy,
'aria-owns': owns,
'aria-haspopup': true,
placeholder: this.props.placeholder,
disabled: this.props.disabled,
readOnly: this.props.readOnly,
role: "combobox",
value: this.props.value,
focused: this.state.focused,
format: this.props.format,
editFormat: this.props.editFormat,
editing: this.state.focused,
parse: this._parse,
onChange: this._change}),
React.DOM.span({className: "rw-select"},
this.props.calendar &&
btn({tabIndex: "-1",
disabled: this.props.disabled,
'aria-disabled': this.props.disabled,
onClick: this._maybeHandle(_.partial(this._click, 'calendar'))},
React.DOM.i({className: "rw-i rw-i-calendar"}, React.DOM.span({className: "rw-sr"}, this.props.messages.calendarButton))
),
this.props.time &&
btn({tabIndex: "-1",
disabled: this.props.disabled,
'aria-disabled': this.props.disabled,
onClick: this._maybeHandle(_.partial(this._click, 'time'))},
React.DOM.i({className: "rw-i rw-i-clock-o"}, React.DOM.span({className: "rw-sr"}, this.props.messages.timeButton))
)
),
this.props.time &&
Popup({
open: this.state.open && this.state.openPopup === 'time',
onRequestClose: this.close,
duration: this.props.duration},
React.DOM.div(null,
Time({ref: "timePopup",
id: timeListID,
optID: timeOptID,
'aria-hidden': !this.state.open,
style: { maxHeight: 200, height: 'auto'},
value: this.props.value,
min: this.props.min,
max: this.props.max,
preserveDate: !!this.props.calendar,
itemComponent: this.props.timeComponent,
onSelect: this._maybeHandle(this._selectTime)})
)
),
this.props.calendar &&
Popup({
className: "rw-calendar-popup",
open: this.state.open && this.state.openPopup === 'calendar',
duration: this.props.duration,
onRequestClose: this.close},
mergeIntoProps(
_.pick(this.props, _.keys(Calendar.type.propTypes)),
Calendar({ref: "calPopup",
id: dateListID,
value: this.props.value || new Date,
maintainFocus: false,
'aria-hidden': !this.state.open,
onChange: this._maybeHandle(this._selectDate)})
)
)
)
)
},
_change: function(date, str, constrain){
var change = this.props.onChange
if(constrain)
date = this.inRangeValue(date)
if( change ) {
if( date == null || this.props.value == null){
if( date != this.props.value )
change(date, str)
}
else if (!dates.eq(date, this.props.value))
this.props.onChange(date)
}
},
_keyDown: function(e){
if( e.key === 'Tab')
return
if ( e.key === 'Escape' && this.state.open )
this.close()
else if ( e.altKey ) {
e.preventDefault()
if ( e.key === 'ArrowDown')
this.open( !this.state.open
? this.state.openPopup
: this.state.openPopup === 'time'
? 'calendar'
: 'time')
else if ( e.key === 'ArrowUp')
this.close()
} else if (this.state.open ) {
if( this.state.openPopup === 'calendar' )
this.refs.calPopup._keyDown(e)
if( this.state.openPopup === 'time' )
this.refs.timePopup._keyDown(e)
}
},
//timeout prevents transitions from breaking focus
_focus: function(focused, e){
var self = this
, input = this.refs.valueInput;
clearTimeout(self.timer)
self.timer = setTimeout(function(){
if(focused) input.getDOMNode().focus()
else self.close()
if( focused !== self.state.focused)
self.setState({ focused: focused })
}, 0)
},
_selectDate: function(date){
this.close()
this._change(
dates.merge(date, this.props.value)
, formatDate(date, this.props.format)
, true)
},
_selectTime: function(datum){
this.close()
this._change(
dates.merge(this.props.value, datum.date)
, formatDate(datum.date, this.props.format)
, true)
},
_click: function(view, e){
this._focus(true)
this.toggle(view, e)
},
_parse: function(string){
var parser = _.isFunction(this.props.parse)
? parse
: _.partial(formatsParser, _.compact([ this.props.format ].concat(this.props.parse)) );
return parser(string)
},
toggle: function(view, e) {
this.state.open
? this.state.view !== view
? this.open(view)
: this.close(view)
: this.open(view)
},
_maybeHandle: function(handler, disabledOnly){
if ( !(this.props.disabled || (!disabledOnly &&this.props.readOnly)))
return handler
},
open: function(view){
this.setState({ open: true, openPopup: view })
},
close: function(){
this.setState({ open: false })
},
_id: function(suffix){
this._id_ || (this._id_ = _.uniqueId('rw_'))
return (this.props.id || this._id_) + suffix
},
inRangeValue: function(value){
if( value == null) return value
return dates.max(
dates.min(value, this.props.max)
, this.props.min)
},
});
var btn = require('../common/btn')
function formatDate(date, format){
var val = ''
if ( (date instanceof Date) && !isNaN(date.getTime()) )
val = dates.format(date, format)
return val;
}
function formatsParser(formats, str){
var date;
formats = [].concat(formats)
for(var i=0; i < formats.length; i++ ){
date = dates.parse(str, formats[i])
if( date) return date
}
return null
}