react-date-picker
Version:
A carefully crafted date picker for React
929 lines (697 loc) • 20 kB
JavaScript
import React, { PropTypes } from 'react'
import { findDOMNode } from 'react-dom'
import Component from 'react-class'
import assign from 'object-assign'
import { Flex } from 'react-flex'
import Input from 'react-field'
import DateFormatInput from '../DateFormatInput'
import InlineBlock from 'react-inline-block'
import { CLEAR_ICON } from './icons'
import moment from 'moment'
import join from '../join'
import toMoment from '../toMoment'
import Calendar, { NAV_KEYS } from '../Calendar'
import joinFunctions from '../joinFunctions'
import assignDefined from '../assignDefined'
import forwardTime from '../utils/forwardTime'
const POSITIONS = { top: 'top', bottom: 'bottom' }
const getPicker = props => {
return React.Children
.toArray(props.children)
.filter(c => c && c.props && c.props.isDatePicker)[0] || <Calendar />
}
const FIND_INPUT = c => c && (c.type === 'input' || (c.props && c.isDateInput))
const preventDefault = (event) => {
event.preventDefault()
}
export default class DateField extends Component {
constructor(props) {
super(props)
this.state = {
value: props.defaultValue === undefined ? '' : props.defaultValue,
expanded: props.defaultExpanded || false,
focused: false
}
}
componentWillUnmount() {
this.unmounted = true
}
render() {
const props = this.prepareProps(this.props)
const flexProps = assign({}, props)
delete flexProps.activeDate
delete flexProps.cleanup
delete flexProps.clearIcon
delete flexProps.collapseOnDateClick
delete flexProps.date
delete flexProps.dateFormat
delete flexProps.expanded
delete flexProps.expandOnFocus
delete flexProps.footer
delete flexProps.forceValidDate
delete flexProps.locale
delete flexProps.onExpand
delete flexProps.onExpandChange
delete flexProps.onCollapse
delete flexProps.minDate
delete flexProps.maxDate
delete flexProps.pickerProps
delete flexProps.position
delete flexProps.showClock
delete flexProps.skipTodayTime
delete flexProps.strict
delete flexProps.valid
delete flexProps.validateOnBlur
delete flexProps.viewDate
delete flexProps.value
delete flexProps.text
delete flexProps.theme
delete flexProps.updateOnDateClick
if (typeof props.cleanup == 'function') {
props.cleanup(flexProps)
}
return <Flex
inline
row
wrap={false}
{...flexProps}
>
{this.renderInput()}
{this.renderClearIcon()}
{this.renderCalendarIcon()}
{this.renderPicker()}
</Flex>
}
renderInput() {
const props = this.p
const inputProps = this.prepareInputProps(props)
let input
if (props.renderInput) {
input = props.renderInput(inputProps)
}
if (input === undefined) {
input = props.children.filter(FIND_INPUT)[0]
const FieldInput = props.forceValidDate ? DateFormatInput : Input
const propsForInput = assign({}, inputProps)
if (!props.forceValidDate) {
delete propsForInput.date
delete propsForInput.maxDate
delete propsForInput.minDate
delete propsForInput.dateFormat
}
input = input ?
React.cloneElement(input, propsForInput) :
<FieldInput {...propsForInput} />
}
return input
}
renderClearIcon() {
const props = this.p
if (!props.clearIcon || props.forceValidDate || props.disabled) {
return undefined
}
const clearIcon = props.clearIcon === true ?
CLEAR_ICON :
props.clearIcon
const clearIconProps = {
style: {
visibility: props.text ? 'visible' : 'hidden'
},
className: 'react-date-field__clear-icon',
onMouseDown: this.onClearMouseDown,
children: clearIcon
}
let result
if (props.renderClearIcon) {
result = props.renderClearIcon(clearIconProps)
}
if (result === undefined) {
result = <InlineBlock {...clearIconProps} />
}
return result
}
onClearMouseDown(event) {
event.preventDefault()
this.onFieldChange('')
if (!this.isFocused()) {
this.focus()
}
}
renderCalendarIcon() {
let result
const renderIcon = this.props.renderCalendarIcon
const calendarIconProps = {
className: 'react-date-field__calendar-icon',
onMouseDown: this.onCalendarIconMouseDown,
children: <div className="react-date-field__calendar-icon-inner" />
}
if (renderIcon) {
result = renderIcon(calendarIconProps)
}
if (result === undefined) {
result = <div {...calendarIconProps} />
}
return result
}
onCalendarIconMouseDown(event) {
if (this.props.disabled) {
return
}
event.preventDefault()
if (!this.isFocused()) {
this.focus()
}
this.toggleExpand()
}
prepareExpanded(props) {
return props.expanded === undefined ?
this.state.expanded :
props.expanded
}
prepareDate(props, pickerProps) {
props = props || this.p
pickerProps = pickerProps || props.pickerProps
const locale = props.locale || pickerProps.locale
const dateFormat = props.dateFormat || pickerProps.dateFormat
let value = props.value === undefined ?
this.state.value :
props.value
const date = this.toMoment(value)
const valid = date.isValid()
if (value && typeof value != 'string' && valid) {
value = this.format(date)
}
if (date && valid) {
this.lastValidDate = date
} else {
value = this.state.value
}
const viewDate = this.state.viewDate || this.lastValidDate || new Date()
const activeDate = this.state.activeDate || this.lastValidDate || new Date()
return {
viewDate,
activeDate,
dateFormat,
locale,
valid,
date,
value
}
}
preparePickerProps(props) {
const picker = getPicker(props, this)
if (!picker) {
return null
}
return picker.props || {}
}
prepareProps(thisProps) {
const props = this.p = assign({}, thisProps)
props.children = React.Children.toArray(props.children)
props.expanded = this.prepareExpanded(props)
props.pickerProps = this.preparePickerProps(props)
const input = props.children.filter(FIND_INPUT)[0]
if (input && input.type == 'input') {
props.rawInput = true
props.forceValidDate = false
}
const dateInfo = this.prepareDate(props, props.pickerProps)
assign(props, dateInfo)
if (props.text === undefined) {
props.text = this.state.text
if (props.text == null) {
props.text = props.valid && props.date ?
props.value :
this.props.value
}
}
if (props.text === undefined) {
props.text = ''
}
props.className = this.prepareClassName(props)
return props
}
prepareClassName(props) {
const position = POSITIONS[props.pickerProps.position || props.pickerPosition] || 'bottom'
return join([
'react-date-field',
props.className,
props.disabled && 'react-date-field--disabled',
props.theme && `react-date-field--theme-${props.theme}`,
`react-date-field--picker-position-${position}`,
this.isLazyFocused() && join(
'react-date-field--focused',
props.focusedClassName
),
this.isExpanded() && join(
'react-date-field--expanded',
props.expandedClassName
),
!props.valid && join(props.invalidClassName, 'react-date-field--invalid')
])
}
prepareInputProps(props) {
const input = props.children.filter(FIND_INPUT)[0]
const inputProps = (input && input.props) || {}
const onBlur = joinFunctions(inputProps.onBlur, this.onFieldBlur)
const onFocus = joinFunctions(inputProps.onFocus, this.onFieldFocus)
const onChange = joinFunctions(inputProps.onChange, this.onFieldChange)
const onKeyDown = joinFunctions(inputProps.onKeyDown, this.onFieldKeyDown)
const newInputProps = assign({}, inputProps, {
ref: (f) => { this.field = f },
date: props.date,
onFocus,
onBlur,
onChange,
dateFormat: props.dateFormat,
value: props.text || '',
onKeyDown,
className: join(
'react-date-field__input',
inputProps.className
)
})
assignDefined(newInputProps, {
placeholder: props.placeholder,
disabled: props.disabled,
minDate: props.minDate,
maxDate: props.maxDate
})
return newInputProps
}
renderPicker() {
const props = this.p
if (this.isExpanded()) {
const newExpand = !this.picker
const picker = getPicker(props, this)
const pickerProps = props.pickerProps
const onMouseDown = joinFunctions(pickerProps.onMouseDown, this.onPickerMouseDown)
const onChange = joinFunctions(pickerProps.onChange, this.onPickerChange)
const date = props.valid && props.date
const footer = pickerProps.footer !== undefined ? pickerProps.footer : props.footer
const viewDate = newExpand && date ? date : props.viewDate
const activeDate = newExpand && date ? date : props.activeDate
return React.cloneElement(picker, assignDefined({
ref: (p) => {
this.picker = this.pickerView = p
if (p && p.getView) {
this.pickerView = p.getView()
}
if (!this.state.viewDate) {
this.onViewDateChange(props.viewDate)
}
},
footer,
focusOnNavMouseDown: false,
focusOnFooterMouseDown: false,
insideField: true,
showClock: props.showClock,
getTransitionTime: this.getTime,
updateOnWheel: props.updateOnWheel,
onClockInputBlur: this.onClockInputBlur,
onClockEnterKey: this.onClockEnterKey,
onClockEscapeKey: this.onClockEscapeKey,
footerClearDate: props.clearDate || props.minDate,
onFooterCancelClick: this.onFooterCancelClick,
onFooterTodayClick: this.onFooterTodayClick,
onFooterOkClick: this.onFooterOkClick,
onFooterClearClick: this.onFooterClearClick,
dateFormat: props.dateFormat,
theme: props.theme || pickerProps.theme,
arrows: props.navBarArrows,
className: join(pickerProps.className, 'react-date-field__picker'),
date: date || null,
tabIndex: -1,
viewDate,
activeDate,
locale: props.locale,
onViewDateChange: this.onViewDateChange,
onActiveDateChange: this.onActiveDateChange,
onTimeChange: this.onTimeChange,
onTransitionStart: this.onTransitionStart,
onMouseDown,
onChange
}, {
minDate: props.minDate,
maxDate: props.maxDate
}))
}
this.time = null
return null
}
onTimeChange(value, timeFormat) {
const timeMoment = this.toMoment(value, { dateFormat: timeFormat })
const time = ['hour', 'minute', 'second', 'millisecond'].reduce((acc, part) => {
acc[part] = timeMoment.get(part)
return acc
}, {})
this.time = time
}
getTime() {
return this.time
}
setValue(value, config = {}) {
const dateMoment = this.toMoment(value)
const dateString = this.format(dateMoment)
this.setDate(dateString, assign(config, { dateMoment }))
}
onFooterOkClick() {
const activeDate = this.p.activeDate
if (activeDate) {
const date = this.toMoment(activeDate)
forwardTime(this.time, date)
this.setValue(date, { skipTime: !!this.time })
}
this.setExpanded(false)
}
onFooterCancelClick() {
this.setExpanded(false)
}
onFooterTodayClick() {
const today = this.toMoment(new Date())
.startOf('day')
this.onPickerChange(this.format(today), { dateMoment: today })
this.onViewDateChange(today)
this.onActiveDateChange(today)
return false
}
onFooterClearClick() {
const clearDate = this.props.clearDate === undefined ? this.props.minDate : this.props.clearDate
if (clearDate !== undefined) {
this.setValue(clearDate, {
skipTime: true
})
}
this.setExpanded(false)
return false
}
toMoment(value, props) {
if (moment.isMoment(value)) {
return value
}
props = props || this.p
let date = toMoment(value, {
strict: props.strict,
locale: props.locale,
dateFormat: props.displayFormat || props.dateFormat || this.p.dateFormat
})
if (!date.isValid() && props.displayFormat) {
date = toMoment(value, {
strict: props.strict,
locale: props.locale,
dateFormat: props.dateFormat || this.p.dateFormat
})
}
return date
}
isValid(text) {
if (text === undefined) {
text = this.p.text
}
return this.toMoment(text).isValid()
}
onViewDateChange(viewDate) {
this.setState({
viewDate
})
}
onActiveDateChange(activeDate) {
this.setState({
activeDate
})
}
onViewKeyDown(event) {
const key = event.key
if (this.pickerView) { // } && (key == 'Escape' || key == 'Enter' || (key in NAV_KEYS))) {
this.pickerView.onViewKeyDown(event)
}
}
onPickerMouseDown(event) {
preventDefault(event)
if (!this.isFocused()) {
this.focus()
}
}
isHistoryViewVisible() {
if (this.picker && this.picker.isHistoryViewVisible) {
return this.picker.isHistoryViewVisible()
}
return false
}
onFieldKeyDown(event) {
const key = event.key
const expanded = this.isExpanded()
const historyVisible = this.isHistoryViewVisible()
if (key == 'Enter' && !historyVisible) {
this.onViewKeyDown(event)
this.toggleExpand()
return false
}
if (historyVisible && (key == 'Escape' || key == 'Enter')) {
this.onViewKeyDown(event)
return false
}
if (key == 'Escape') {
if (expanded) {
this.setExpanded(false)
return false
}
}
if (expanded) {
if (key in NAV_KEYS) {
this.onViewKeyDown(event)
return false
}
// if (!currentPosition || !currentPosition.time) {
// // the time has not changed, so it's safe to forward the event
// this.onViewKeyDown(event)
// return false
// }
}
return true
}
getInput() {
return findDOMNode(this.field)
}
isFocused() {
return this.state.focused
}
isLazyFocused() {
return this.isFocused() || this.isTimeInputFocused()
}
isTimeInputFocused() {
if (this.pickerView && this.pickerView.isTimeInputFocused) {
return this.pickerView.isTimeInputFocused()
}
return false
}
onFieldFocus(event) {
if (this.state.focused) {
return
}
this.setState({
focused: true
})
if (this.props.expandOnFocus) {
this.setExpanded(true)
}
this.props.onFocus(event)
}
onFieldBlur(event) {
if (!this.isFocused()) {
return
}
this.setState({
focused: false
})
this.props.onBlur(event)
if (!this.pickerView || !this.pickerView.isTimeInputFocused) {
this.onLazyBlur()
return
}
setTimeout(() => this.onLazyBlur(), 0)
}
onClockEnterKey() {
if (!this.isFocused()) {
this.focus()
}
this.onFooterOkClick()
}
onClockEscapeKey() {
if (!this.isFocused()) {
this.focus()
}
this.onFooterCancelClick()
}
onClockInputBlur() {
setTimeout(() => {
if (!this.isFocused()) {
this.onLazyBlur()
}
}, 0)
}
onLazyBlur() {
if (this.unmounted) {
return
}
if (this.isTimeInputFocused()) {
return
}
this.setExpanded(false)
if (!this.isValid() && this.props.validateOnBlur) {
const value = this.lastValidDate && this.p.text != '' ?
this.format(this.lastValidDate) :
''
setTimeout(() => {
this.onFieldChange(value)
}, 0)
}
}
onInputChange() {
}
isExpanded() {
return this.p.expanded
}
toggleExpand() {
this.setExpanded(!this.p.expanded)
}
setExpanded(bool) {
const props = this.p
if (bool === props.expanded) {
return
}
if (!bool) {
this.onCollapse()
} else {
this.setState({}, () => {
this.onExpand()
})
}
if (bool && props.valid) {
this.setState({
// viewDate: props.date,
activeDate: props.date
})
}
if (this.props.expanded === undefined) {
this.setState({
expanded: bool
})
}
this.props.onExpandChange(bool)
}
onCollapse() {
this.props.onCollapse()
}
onExpand() {
this.props.onExpand()
}
onFieldChange(value) {
if (this.p.rawInput && typeof value != 'string') {
const event = value
value = event.target.value
}
const dateMoment = value == '' ?
null :
this.toMoment(value)
if (dateMoment === null || dateMoment.isValid()) {
this.onChange(dateMoment)
}
this.onTextChange(value)
}
onTextChange(text) {
if (this.props.text === undefined && this.props.value === undefined) {
this.setState({
text
})
}
if (this.props.onTextChange) {
this.props.onTextChange(text)
}
}
onPickerChange(dateString, { dateMoment, forceUpdate }, event) {
const isEnter = event && event.key == 'Enter'
const updateOnDateClick = forceUpdate ? true : this.props.updateOnDateClick || isEnter
if (updateOnDateClick) {
forwardTime(this.time, dateMoment)
this.setDate(dateString, { dateMoment })
if (this.props.collapseOnDateClick || isEnter) {
this.setExpanded(false)
}
}
}
setDate(dateString, { dateMoment, skipTime = false }) {
const props = this.p
const currentDate = props.date
if (props.valid && currentDate) {
const dateFormat = props.dateFormat.toLowerCase()
const hasTime = dateFormat.indexOf('k') != -1 ||
dateFormat.indexOf('h') != -1
if (hasTime && !skipTime) {
['hour', 'minute', 'second', 'millisecond'].forEach(part => {
dateMoment.set(part, currentDate.get(part))
})
}
}
this.onTextChange(this.format(dateMoment))
this.onChange(dateMoment)
}
onChange(dateMoment) {
if (dateMoment != null && !moment.isMoment(dateMoment)) {
dateMoment = this.toMoment(dateMoment)
}
forwardTime(this.time, dateMoment)
const newState = {}
if (this.props.value === undefined) {
assign(newState, {
text: null,
value: dateMoment
})
}
newState.activeDate = dateMoment
if (!this.pickerView || !this.pickerView.isInView || !this.pickerView.isInView(dateMoment)) {
newState.viewDate = dateMoment
}
if (this.props.onChange) {
this.props.onChange(this.format(dateMoment), { dateMoment })
}
this.setState(newState)
}
format(mom, format) {
return mom == null ?
'' :
mom.format(format || this.p.displayFormat || this.p.dateFormat)
}
focusField() {
const input = findDOMNode(this.field)
if (input) {
input.focus()
}
}
focus() {
this.focusField()
}
}
DateField.defaultProps = {
showClock: undefined,
forceValidDate: false,
strict: true,
expandOnFocus: true,
updateOnDateClick: false,
collapseOnDateClick: false,
theme: 'default',
footer: true,
onBlur: () => {},
onFocus: () => {},
clearIcon: true,
validateOnBlur: true,
onExpandChange: () => {},
onCollapse: () => {},
onExpand: () => {},
minDate: moment('1000-01-01', 'YYYY-MM-DD'),
maxDate: moment('9999-12-31 HH:mm:ss', 'YYYY-MM-DD 23:59:59'),
skipTodayTime: false
}
DateField.propTypes = {
dateFormat: PropTypes.string.isRequired
}