UNPKG

react-date-picker

Version:

A carefully crafted date picker for React

1,208 lines (916 loc) 28.8 kB
import React, { PropTypes } from 'react' import { findDOMNode } from 'react-dom' import Component from 'react-class' import moment from 'moment' import assign from 'object-assign' import clampRange from '../clampRange' import toMoment from '../toMoment' import join from '../join' import isInRange from '../utils/isInRange' import NavBar from '../NavBar' import Footer from '../Footer' import bemFactory from '../bemFactory' import joinFunctions from '../joinFunctions' import assignDefined from '../assignDefined' import BasicMonthView, { getDaysInMonthView, getWeekendStartDay } from '../BasicMonthView' import ON_KEY_DOWN from './onKeyDown' import NAV_KEYS from './navKeys' let TODAY const RENDER_DAY = (props) => { const divProps = assign({}, props) delete divProps.date delete divProps.dateMoment delete divProps.day delete divProps.isAfterMaxDate delete divProps.isBeforeMinDate delete divProps.inRange delete divProps.timestamp return <div {...divProps} /> } const isDateInMinMax = function (timestamp, props) { if (props.minDate && timestamp < props.minDate) { return false } if (props.maxDate && timestamp > props.maxDate) { return false } return true } const isValidActiveDate = function (timestamp, props) { if (!props) { throw new Error('props is mandatory in isValidActiveDate') } const dayProps = props.dayPropsMap[timestamp] if (dayProps && dayProps.disabled) { return false } return isDateInMinMax(timestamp, props) } const isInView = function (mom, props) { if (!props) { throw new Error('props is mandatory in isInView') } const daysInView = props.daysInView return isInRange(mom, { range: daysInView, inclusive: true }) } const prepareViewDate = function (props, state) { const viewDate = props.viewDate === undefined ? state.viewDate : props.viewDate if (!viewDate && props.moment) { return toMoment(props.moment) } return viewDate } const prepareDate = function (props, state) { if (props.range) { return null } return props.date === undefined ? state.date : props.date } const prepareRange = function (props, state) { if (props.moment) { return null } return props.partialRange ? props.range || state.range : state.range || props.range } const prepareActiveDate = function (props, state) { const fallbackDate = prepareDate(props, state) || ((prepareRange(props, state) || [])[0]) const activeDate = props.activeDate === undefined ? // only fallback to date if activeDate not specified (state.activeDate || fallbackDate) : props.activeDate const daysInView = props.daysInView if (activeDate && daysInView && props.constrainActiveInView) { const activeMoment = this.toMoment(activeDate) if (!isInView(activeMoment, props)) { const date = fallbackDate const dateMoment = this.toMoment(date) if (date && isInView(dateMoment, props) && isValidActiveDate(+dateMoment, props)) { return date } return null } } return isValidActiveDate(+activeDate, props) ? activeDate : null } const renderFooter = (props, buttonHandlers) => { if (!props.footer) { return null } buttonHandlers = buttonHandlers || props const renderFooter = props.renderFooter const footerFnProps = { onTodayClick: buttonHandlers.onFooterTodayClick, onClearClick: buttonHandlers.onFooterClearClick, onOkClick: buttonHandlers.onFooterOkClick, onCancelClick: buttonHandlers.onFooterCancelClick } const childFooter = React.Children.toArray(props.children) .filter(c => c && c.props && c.props.isDatePickerFooter)[0] const childFooterProps = childFooter ? childFooter.props : null if (childFooterProps) { // also take into account the props on childFooter // so we merge those with the other props already built Object.keys(footerFnProps).forEach(key => { if (childFooter.props[key]) { footerFnProps[key] = joinFunctions(footerFnProps[key], childFooter.props[key]) } }) } const footerProps = assignDefined({}, footerFnProps, { todayButton: props.todayButton, todayButtonText: props.todayButtonText, clearButton: props.clearButton, clearButtonText: props.clearButtonText, okButton: props.okButton === undefined && !props.insideField ? false : props.okButton, okButtonText: props.okButtonText, cancelButton: props.cancelButton === undefined && !props.insideField ? false : props.cancelButton, cancelButtonText: props.cancelButtonText, clearDate: props.clearDate || props.footerClearDate, selectDate: props.selectDate }) if (childFooter) { if (renderFooter) { return renderFooter(assign({}, childFooter.props, footerProps)) } return React.cloneElement(childFooter, footerProps) } if (renderFooter) { return renderFooter(footerProps) } return <Footer {...footerProps} /> } export default class MonthView extends Component { isInView(mom, props) { return isInView(mom, props || this.p) } constructor(props) { super(props) this.state = { range: props.defaultRange, date: props.defaultDate, hoverRange: props.defaultHoverRange, activeDate: props.defaultActiveDate, viewDate: props.defaultViewDate } } componentWillMount() { this.updateBem(this.props) this.updateToMoment(this.props) } componentWillReceiveProps(nextProps) { if (nextProps.defaultClassName != this.props.defaultClassName) { this.updateBem(nextProps) } this.updateToMoment(nextProps) } updateBem(props) { this.bem = bemFactory(props.defaultClassName) } updateToMoment(props) { this.toMoment = (value, dateFormat) => { return toMoment(value, { locale: props.locale, dateFormat: dateFormat || props.dateFormat }) } TODAY = +this.toMoment().startOf('day') } prepareClassName(props) { return join( props.className, this.bem(), this.bem(null, `theme-${props.theme}`) ) } prepareProps(thisProps, state) { const props = this.p = assign({}, thisProps) state = state || this.state props.hoverRange = props.hoverRange === undefined ? this.state.hoverRange : props.hoverRange props.dayPropsMap = {} props.className = this.prepareClassName && this.prepareClassName(props) const { minDate, maxDate } = props if (minDate) { props.minDateMoment = this.toMoment(props.minDate).startOf('day') props.minDate = +props.minDateMoment } if (maxDate) { props.maxDateMoment = this.toMoment(props.maxDate) props.maxDate = +props.maxDateMoment } const date = prepareDate(props, state) if (date) { props.moment = props.moment || (props.range ? null : this.toMoment(date).startOf('day')) props.timestamp = props.moment ? +props.moment : null } props.viewMoment = props.viewMoment || this.toMoment(prepareViewDate(props, state)) if (props.constrainViewDate && props.minDate && props.viewMoment.isBefore(props.minDate)) { props.minConstrained = true props.viewMoment = this.toMoment(props.minDate) } if (props.constrainViewDate && props.maxDate && props.viewMoment.isAfter(props.maxDate)) { props.maxConstrained = true props.viewMoment = this.toMoment(props.maxDate) } props.viewMonthStart = this.toMoment(props.viewMoment).startOf('month') props.viewMonthEnd = this.toMoment(props.viewMoment).endOf('month') const range = prepareRange(props, state) if (range) { props.range = range.map(d => this.toMoment(d).startOf('day')) props.rangeStart = state.rangeStart || (props.range.length == 1 ? props.range[0] : null) } props.daysInView = getDaysInMonthView(props.viewMoment, props) const activeDate = prepareActiveDate.call(this, props, state) if (activeDate) { props.activeDate = +this.toMoment(activeDate).startOf('day') } return props } getViewMoment() { return this.p.viewMoment } getViewSize() { return 1 } // handleViewMouseLeave(){ // this.state.range && this.setState({ range: null }) // } preparePrevNextClassName(timestamp, props) { const { viewMonthStart, viewMonthEnd } = props const before = timestamp < viewMonthStart const after = timestamp > viewMonthEnd const thisMonth = !before && !after return join( timestamp == TODAY && this.bem('day--today'), props.highlightToday && timestamp == TODAY && this.bem('day--today-highlight'), before && this.bem('day--prev-month'), before && !props.showDaysBeforeMonth && this.bem('day--hidden'), after && this.bem('day--next-month'), after && !props.showDaysAfterMonth && this.bem('day--hidden'), thisMonth && this.bem('day--this-month') ) } prepareMinMaxProps(timestamp, props) { const classes = [] let isBeforeMinDate = false let isAfterMaxDate = false const { minDate, maxDate } = props if (minDate && timestamp < minDate) { classes.push( this.bem('day--disabled-min') ) isBeforeMinDate = true } if (maxDate && timestamp > maxDate) { classes.push( this.bem('day--disabled-max') ) isAfterMaxDate = true } return { className: join(classes), isBeforeMinDate, isAfterMaxDate, disabled: isBeforeMinDate || isAfterMaxDate } } prepareWeekendClassName(dateMoment, { highlightWeekends }) { // const props = this.p const weekDay = dateMoment.day() // const weekendStartDay = getWeekendStartDay(props) if (weekDay === 0 /* Sunday */ || weekDay === 6 /* Saturday */) { // if (weekDay === weekendStartDay || weekDay === weekendStartDay + 1) { return join( this.bem('day--weekend'), highlightWeekends && this.bem('day--weekend-highlight') ) } return '' } prepareRangeProps(dateMoment, props) { let inRange = false const className = [] const { hoverRange, range } = props if (range) { let [rangeStart, rangeEnd] = range if (!range.length) { rangeStart = props.rangeStart } // const rangeName = !props.partialRange ? 'hover-range' : 'range' const rangeName = 'range' //hoverRange ? 'range' : 'hover-range' if (rangeStart && dateMoment.isSame(rangeStart)) { className.push(this.bem(`day--${rangeName}-start`)) className.push(this.bem(`day--in-${rangeName}`)) if (!rangeEnd) { className.push(this.bem(`day--${rangeName}-end`)) } inRange = true } if (rangeEnd && dateMoment.isSame(rangeEnd)) { className.push(this.bem(`day--${rangeName}-end`)) className.push(this.bem(`day--in-${rangeName}`)) inRange = true } if (!inRange && isInRange(dateMoment, range)) { className.push(this.bem(`day--in-${rangeName}`)) inRange = true } } if (range && range.length < 2 && hoverRange && isInRange(dateMoment, hoverRange)) { className.push(this.bem('day--in-hover-range')) if (dateMoment.isSame(hoverRange[0])) { className.push(this.bem('day--hover-range-start')) } if (dateMoment.isSame(hoverRange[1])) { className.push(this.bem('day--hover-range-end')) } } return { inRange, className: join(className) } } prepareDayProps(renderDayProps, props) { const { timestamp, dateMoment, className } = renderDayProps props = props || this.p const result = {} const minMaxProps = this.prepareMinMaxProps(timestamp, props) const rangeProps = this.prepareRangeProps(dateMoment, props) const weekendClassName = this.prepareWeekendClassName(dateMoment, props) const prevNextClassName = this.preparePrevNextClassName(timestamp, props) const currentTimestamp = props.timestamp assign( result, minMaxProps, rangeProps, { children: <div className={this.bem('day-text')}> {renderDayProps.day} </div>, className: join([ minMaxProps.className, rangeProps.className, prevNextClassName, weekendClassName, timestamp == currentTimestamp ? this.bem('day--value') : null, timestamp == props.activeDate ? this.bem('day--active') : null, className ]) } ) if (!result.disabled && props.isDisabledDay) { result.disabled = props.isDisabledDay(renderDayProps, props) } return result } focus() { const domNode = findDOMNode(this) if (domNode) { domNode.focus() } } onDayTextMouseEnter({ dateMoment, timestamp }) { if (!this.state.focused) { this.focus() } this.onActiveDateChange({ dateMoment, timestamp }) } renderDay(renderProps) { const props = this.p const { dateMoment, timestamp } = renderProps assign( renderProps, this.prepareDayProps(renderProps, props) ) if (props.range && props.highlightRangeOnMouseMove) { renderProps.onMouseEnter = this.handleDayMouseEnter.bind(this, renderProps) } if (typeof props.onRenderDay === 'function') { renderProps = props.onRenderDay(renderProps) } if (renderProps.disabled) { renderProps.className = join( this.bem('day--disabled'), renderProps.className ) } else { const eventParam = { dateMoment, timestamp } const onClick = this.handleClick.bind(this, eventParam) const prevOnClick = renderProps.onClick renderProps.onClick = prevOnClick ? (...args) => { prevOnClick(...args) onClick(...args) } : onClick if (props.activateOnHover && this.props.activeDate !== null) { const onMouseEnter = this.onDayTextMouseEnter.bind(this, eventParam) const prevOnMouseEnter = renderProps.onMouseEnter renderProps.onMouseEnter = prevOnMouseEnter ? (...args) => { prevOnMouseEnter(...args) onMouseEnter(...args) } : onMouseEnter } } props.dayPropsMap[timestamp] = renderProps const renderFunction = props.renderDay || RENDER_DAY let result = renderFunction(renderProps) if (result === undefined) { result = RENDER_DAY(renderProps) } return result } render() { const props = this.p = this.prepareProps(this.props) const basicViewProps = assign({}, props) delete basicViewProps.activeDate delete basicViewProps.activateOnHover delete basicViewProps.arrows delete basicViewProps.cleanup delete basicViewProps.constrainViewDate delete basicViewProps.constrainActiveInView delete basicViewProps.dayPropsMap delete basicViewProps.date delete basicViewProps.defaultActiveDate delete basicViewProps.defaultDate delete basicViewProps.defaultRange delete basicViewProps.defaultViewDate delete basicViewProps.enableHistoryView delete basicViewProps.focusOnFooterMouseDown delete basicViewProps.focusOnNavMouseDown delete basicViewProps.footer delete basicViewProps.footerClearDate delete basicViewProps.getTransitionTime delete basicViewProps.highlightRangeOnMouseMove delete basicViewProps.highlightToday delete basicViewProps.highlightWeekends delete basicViewProps.hoverRange delete basicViewProps.insideField delete basicViewProps.isDatePicker delete basicViewProps.isDisabledDay delete basicViewProps.maxConstrained delete basicViewProps.maxDate delete basicViewProps.maxDateMoment delete basicViewProps.minConstrained delete basicViewProps.minDate delete basicViewProps.minDateMoment delete basicViewProps.navBarArrows delete basicViewProps.navNext delete basicViewProps.navigation delete basicViewProps.navOnDateClick delete basicViewProps.navPrev delete basicViewProps.onActiveDateChange delete basicViewProps.onChange delete basicViewProps.onRangeChange delete basicViewProps.onViewDateChange delete basicViewProps.onTransitionStart delete basicViewProps.partialRange delete basicViewProps.range delete basicViewProps.rangeStart delete basicViewProps.renderNavBar delete basicViewProps.showDaysAfterMonth delete basicViewProps.showDaysBeforeMonth delete basicViewProps.theme delete basicViewProps.viewDate delete basicViewProps.viewMonthEnd delete basicViewProps.viewMonthStart if (typeof props.cleanup == 'function') { props.cleanup(basicViewProps) } return <BasicMonthView tabIndex={0} {...basicViewProps} renderChildren={this.renderChildren} onKeyDown={this.onViewKeyDown} onFocus={this.onFocus} onBlur={this.onBlur} renderDay={this.renderDay} viewMoment={props.viewMoment} onMouseLeave={props.highlightRangeOnMouseMove && this.handleViewMouseLeave} /> } handleViewMouseLeave(event) { if (this.props.onMouseLeave) { this.props.onMouseLeave(event) } if (this.state.hoverRange) { this.setHoverRange(null) } } renderChildren(children) { const props = this.p const navBar = this.renderNavBar(props) const footer = this.renderFooter(props) const result = [ navBar, children, footer ] if (props.renderChildren) { return props.renderChildren(result) } return result } focusFromFooter() { if (!this.isFocused() && this.props.focusOnFooterMouseDown) { this.focus() } } onFooterTodayClick() { this.focusFromFooter() if (this.props.onFooterTodayClick) { if (this.props.onFooterTodayClick() === false) { return } } this.select({ dateMoment: this.toMoment(Date.now()) }) } onFooterClearClick() { this.focusFromFooter() if (this.props.onFooterClearClick) { if (this.props.onFooterClearClick() === false) { return } } this.select({ dateMoment: null }) } onFooterOkClick() { this.focusFromFooter() if (this.props.onFooterOkClick) { this.props.onFooterOkClick() } } onFooterCancelClick() { if (this.props.onFooterCancelClick) { this.props.onFooterCancelClick() } } renderFooter(props) { return renderFooter(assign({}, props, { selectDate: this.select, owner: this }), this) } renderNavBar(props) { const theme = props.theme const childNavBar = React.Children.toArray(props.children) .filter(c => c && c.props && c.props.isDatePickerNavBar)[0] const ref = (navBar) => { this.navBar = navBar } if (!childNavBar) { if (props.navigation || props.renderNavBar) { return this.renderNavBarComponent(assignDefined({ // prevDisabled, // nextDisabled, minDate: props.minDate, maxDate: props.maxDate, theme, secondary: true, date: props.moment, viewMoment: props.viewMoment, onViewDateChange: this.onNavViewDateChange, onMouseDown: this.onNavMouseDown, arrows: props.navBarArrows, ref }, { enableHistoryView: props.enableHistoryView })) } return null } const navBarProps = assign({}, childNavBar.props, assignDefined({ viewMoment: props.viewMoment, date: props.moment, theme, ref, minDate: props.minDate, maxDate: props.maxDate, }, { enableHistoryView: props.enableHistoryView })) const prevOnViewDateChange = navBarProps.onViewDateChange let onViewDateChange = this.onViewDateChange if (prevOnViewDateChange) { onViewDateChange = (...args) => { prevOnViewDateChange(...args) this.onNavViewDateChange(...args) } } navBarProps.onViewDateChange = onViewDateChange const prevOnMouseDown = navBarProps.onMouseDown let onMouseDown = this.onNavMouseDown if (prevOnMouseDown) { onMouseDown = (...args) => { prevOnMouseDown(...args) this.onNavMouseDown(...args) } } navBarProps.onMouseDown = onMouseDown if (navBarProps) { return this.renderNavBarComponent(navBarProps) } return null } onNavMouseDown(event) { if (this.props.focusOnNavMouseDown && !this.isFocused()) { this.focus() } } renderNavBarComponent(navBarProps) { if (this.props.renderNavBar) { return this.props.renderNavBar(navBarProps) } return <NavBar {...navBarProps} /> } isFocused() { return this.state.focused } onFocus(event) { this.setState({ focused: true }) this.props.onFocus(event) } onBlur(event) { this.setState({ focused: false }) this.hideHistoryView() this.props.onBlur(event) } showHistoryView() { if (this.navBar) { this.navBar.showHistoryView() } } hideHistoryView() { if (this.navBar) { this.navBar.hideHistoryView() } } isHistoryViewVisible() { if (this.navBar) { return this.navBar.isHistoryViewVisible() } return false } tryNavBarKeyDown(event) { if (this.navBar && this.navBar.getHistoryView) { const historyView = this.navBar.getHistoryView() if (historyView && historyView.onKeyDown) { historyView.onKeyDown(event) return true } } return false } onViewKeyDown(event) { if (this.tryNavBarKeyDown(event)) { return } return ON_KEY_DOWN.call(this, event) } confirm(date, event) { event.preventDefault() if (this.props.confirm) { return this.props.confirm(date, event) } const dateMoment = this.toMoment(date) this.select({ dateMoment, timestamp: +dateMoment }, event) return undefined } navigate(dir, event) { const props = this.p const getNavigationDate = (dir, date, dateFormat) => { const mom = moment.isMoment(date) ? date : this.toMoment(date, dateFormat) return typeof dir == 'function' ? dir(mom) : mom.add(dir, 'day') } if (props.navigate) { return props.navigate(dir, event, getNavigationDate) } event.preventDefault() if (props.activeDate) { const nextMoment = getNavigationDate(dir, props.activeDate) this.gotoViewDate({ dateMoment: nextMoment }) } return undefined } handleDayMouseEnter(dayProps) { const props = this.p const { rangeStart, range } = props const partial = !!(rangeStart && range.length < 2) if (partial) { this.setHoverRange(clampRange([rangeStart, dayProps.dateMoment])) } } handleClick({ timestamp, dateMoment }, event) { const props = this.p if (props.minDate && timestamp < props.minDate) { return } if (props.maxDate && timestamp > props.maxDate) { return } event.target.value = timestamp this.select({ dateMoment, timestamp }, event) } select({ dateMoment, timestamp }, event) { if (dateMoment && timestamp === undefined) { timestamp = +dateMoment } if (this.props.select) { return this.props.select({ dateMoment, timestamp }, event) } if (!timestamp) { timestamp = +dateMoment } this.gotoViewDate({ dateMoment, timestamp }) const props = this.p const range = props.range if (range) { this.selectRange({ dateMoment, timestamp }, event) } else { this.onChange({ dateMoment, timestamp }, event) } return undefined } selectRange({ dateMoment, timestamp }, event) { const props = this.p const range = props.range const rangeStart = props.rangeStart if (dateMoment == null) { this.setState({ rangeStart: null }) this.onRangeChange([], event) return } if (!rangeStart) { this.setState({ rangeStart: dateMoment }) if (range.length == 2 ) { this.onRangeChange([], event) } } else { this.setState({ rangeStart: null }) this.onRangeChange( clampRange([rangeStart, dateMoment]), event ) } } format(mom) { return mom == null ? '' : mom.format(this.props.dateFormat) } setHoverRange(hoverRange) { if (this.props.hoverRange === undefined) { this.setState({ hoverRange }) } if (this.props.onHoverRangeChange) { this.props.onHoverRangeChange(hoverRange) } } onRangeChange(range, event) { this.setState({ range: this.props.range === undefined ? range : null }) this.setHoverRange(null) if (this.props.onRangeChange) { const newRange = range.map(m => { const dateMoment = this.toMoment(m) return { dateString: dateMoment.format(this.props.dateFormat), dateMoment, timestamp: +dateMoment } }) const formatted = newRange.map(o => o.dateString) this.props.onRangeChange(formatted, newRange, event) } } onChange({ dateMoment, timestamp }, event) { if (this.props.date === undefined) { this.setState({ date: timestamp }) } if (this.props.onChange) { const dateString = this.format(dateMoment) this.props.onChange(dateString, { dateMoment, timestamp, dateString }, event) } } onNavViewDateChange(dateString, { dateMoment, timestamp }) { this.onViewDateChange({ dateMoment, timestamp }) } onViewDateChange({ dateMoment, timestamp }) { let minDate let maxDate if (this.p.minDateMoment) { minDate = +this.toMoment(this.p.minDateMoment).startOf('month') } if (this.p.maxDateMoment) { maxDate = +this.toMoment(this.p.maxDateMoment).endOf('month') } if (this.props.constrainViewDate && !isDateInMinMax(timestamp, { minDate, maxDate })) { return } if (this.props.viewDate === undefined && this.props.navOnDateClick) { this.setState({ viewDate: timestamp }) } if (this.props.onViewDateChange) { const dateString = this.format(dateMoment) this.props.onViewDateChange(dateString, { dateMoment, dateString, timestamp }) } } isValidActiveDate(date, props) { return isValidActiveDate(date, props || this.p) } onActiveDateChange({ dateMoment, timestamp }) { if (!isValidActiveDate(timestamp, this.p)) { return } const props = this.p const range = props.range if (range && props.rangeStart) { const newRange = clampRange([props.rangeStart, dateMoment]) if (props.partialRange) { this.onRangeChange(newRange) } this.setState({ rangeStart: props.rangeStart, range: newRange }) } if (this.props.activeDate === undefined) { this.setState({ activeDate: timestamp }) } if (this.props.onActiveDateChange) { const dateString = this.format(dateMoment) this.props.onActiveDateChange(dateString, { dateMoment, timestamp, dateString }) } } gotoViewDate({ dateMoment, timestamp }) { if (!timestamp) { timestamp = dateMoment == null ? null : +dateMoment } this.onViewDateChange({ dateMoment, timestamp }) this.onActiveDateChange({ dateMoment, timestamp }) } } MonthView.defaultProps = { defaultClassName: 'react-date-picker__month-view', dateFormat: 'YYYY-MM-DD', theme: 'default', onBlur: () => {}, onFocus: () => {}, footerClearDate: null, partialRange: true, activateOnHover: false, constrainActiveInView: false, showDaysBeforeMonth: true, showDaysAfterMonth: true, highlightWeekends: true, highlightToday: true, navOnDateClick: true, navigation: true, constrainViewDate: true, highlightRangeOnMouseMove: false, isDatePicker: true, enableHistoryView: true, focusOnNavMouseDown: true, focusOnFooterMouseDown: true } MonthView.propTypes = { navOnDateClick: PropTypes.bool, isDisabledDay: PropTypes.func, onChange: PropTypes.func, onViewDateChange: PropTypes.func, onActiveDateChange: PropTypes.func } export { NAV_KEYS, renderFooter }