UNPKG

react-date-picker

Version:

A carefully crafted date picker for React

929 lines (697 loc) 20 kB
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 }