react-date-picker
Version:
A carefully crafted date picker for React
587 lines (435 loc) • 13.8 kB
JavaScript
import React from 'react'
import Component from 'react-class'
import { Flex } from 'react-flex'
import InlineBlock from 'react-inline-block'
import assign from 'object-assign'
import clampRange from './clampRange'
import NavBar from './NavBar'
import toMoment from './toMoment'
import join from './join'
import isInRange from './utils/isInRange'
import { getDaysInMonthView } from './BasicMonthView'
import MonthView, { renderFooter } from './MonthView'
const times = (count) => [...new Array(count)].map((v, i) => i)
const prepareDate = function (props, state) {
if (props.range) {
return null
}
return props.date === undefined ?
state.date :
props.date
}
const prepareViewDate = function (props, state) {
return props.viewDate === undefined ?
state.viewDate :
state.propViewDate || props.viewDate
}
const prepareRange = function (props, state) {
return props.range && props.range.length ? props.range : state.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
if (activeDate && props.inViewStart && props.inViewEnd && props.constrainActiveInView) {
const activeMoment = this.toMoment(activeDate)
if (!isInRange(activeMoment, [props.inViewStart, props.inViewEnd])) {
const date = fallbackDate
const dateMoment = this.toMoment(date)
if (date && isInRange(dateMoment, [props.inViewStart, props.inViewEnd])) {
return date
}
return null
}
}
return activeDate
}
const prepareViews = function (props) {
const daysInView = []
const viewMoments = []
const viewMoment = props.viewMoment
let index = 0
const size = props.size
while (index < size) {
const mom = this.toMoment(viewMoment).startOf('day').add(index, 'month')
const days = getDaysInMonthView(mom, props)
viewMoments.push(mom)
daysInView.push(days)
index++
}
props.daysInView = daysInView
props.viewMoments = viewMoments
const lastViewDays = daysInView[size - 1]
props.inViewStart = daysInView[0][0]
props.inViewEnd = lastViewDays[lastViewDays.length - 1]
}
export const renderNavBar = function(config, navBarProps) {
const props = this.props
const { index, viewMoment } = config
navBarProps = assign({}, navBarProps, {
secondary: true,
minDate: config.minDate || props.minDate,
maxDate: config.maxDate || props.maxDate,
renderNavNext: config.renderHiddenNav || this.renderHiddenNav,
renderNavPrev: config.renderHiddenNav || this.renderHiddenNav,
viewMoment,
onViewDateChange: config.onViewDateChange || this.onNavViewDateChange,
onUpdate: config.onUpdate || this.updateViewMoment,
enableHistoryView: props.enableHistoryView
})
if (index == 0) {
delete navBarProps.renderNavPrev
}
if (index == props.perRow - 1) {
delete navBarProps.renderNavNext
}
return <NavBar {...navBarProps} />
}
export default class MultiMonthView extends Component {
constructor(props) {
super(props)
this.state = {
hoverRange: null,
range: props.defaultRange,
date: props.defaultDate,
activeDate: props.defaultActiveDate,
viewDate: props.defaultViewDate
}
}
componentWillMount() {
this.updateToMoment(this.props)
}
componentWillReceiveProps(nextProps) {
if (nextProps.locale != this.props.locale || nextProps.dateFormat != this.props.dateFormat) {
this.updateToMoment(nextProps)
}
// if (nextProps.viewDate && !nextProps.forceViewUpdate){
// //this is here in order not to change view if already in view
// const viewMoment = this.toMoment(nextProps.viewDate)
// if (this.isInRange(viewMoment) && !nextProps.forceViewUpdate){
// console.log(this.format(viewMoment), this.format(this.p.viewStart),
// this.format(this.p.viewEnd))
// this.setState({
// propViewDate: this.p.viewMoment
// })
// } else {
// debugger
// this.setState({
// propViewDate: null
// })
// }
// }
}
updateToMoment(props) {
this.toMoment = (value, dateFormat) => {
return toMoment(value, {
locale: props.locale,
dateFormat: dateFormat || props.dateFormat
})
}
}
prepareProps(thisProps, state) {
const props = assign({}, thisProps)
state = state || this.state
props.viewMoment = this.toMoment(prepareViewDate(props, state))
// viewStart is the first day of the first month displayed
// viewEnd is the last day of the last month displayed
props.viewStart = this.toMoment(props.viewMoment).startOf('month')
props.viewEnd = this.toMoment(props.viewStart).add(props.size - 1, 'month').endOf('month')
// but we also have inViewStart, which can be a day before viewStart
// which is in displayed as belonging to the prev month
// but is displayed in the current view since it's on the same week
// as viewStart
//
// same for inViewEnd, which is a day after viewEnd - the last day in the same week
prepareViews.call(this, props)
const activeDate = prepareActiveDate.call(this, props, state)
if (activeDate) {
props.activeDate = +this.toMoment(activeDate)
}
props.date = prepareDate(props, state)
if (!props.date) {
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)
}
}
return props
}
render() {
this.views = []
const props = this.p = this.prepareProps(this.props, this.state)
const size = props.size
const rowCount = Math.ceil(size / props.perRow)
const children = times(rowCount).map(this.renderRow).filter(x => !!x)
const className = join(
props.className,
'react-date-picker__multi-month-view',
props.theme && `react-date-picker__multi-month-view--theme-${props.theme}`
)
const footer = renderFooter(props, this)
if (footer) {
children.push(footer)
}
return <Flex
column
inline
alignItems="stretch"
wrap={false}
{...props}
className={className}
children={children}
/>
}
renderRow(rowIndex) {
const props = this.p
const children = times(props.perRow).map(i => {
const index = (rowIndex * props.perRow) + i
if (index >= props.size) {
return null
}
return this.renderView(index, props.size)
})
return <Flex inline row wrap={false} children={children} />
}
renderView(index, size) {
const props = this.p
const viewMoment = props.viewMoments[index]
let range
if (props.range) {
range = props.rangeStart && props.range.length == 0 ?
[props.rangeStart] :
props.range
}
return <MonthView
ref={view => { this.views[index] = view }}
constrainViewDate={false}
{...this.props}
className={null}
index={index}
footer={false}
constrainActiveInView={false}
navigate={this.onMonthNavigate.bind(this, index)}
hoverRange={this.state.hoverRange}
onHoverRangeChange={this.setHoverRange}
activeDate={props.activeDate}
onActiveDateChange={this.onActiveDateChange}
onViewDateChange={this.onAdjustViewDateChange}
date={props.date}
defaultDate={null}
onChange={this.onChange}
range={range}
defaultRange={null}
onRangeChange={this.onRangeChange}
viewMoment={viewMoment}
insideMultiView
daysInView={props.daysInView[index]}
showDaysBeforeMonth={index == 0}
showDaysAfterMonth={index == size - 1}
select={this.select}
renderNavBar={this.props.navigation && (this.props.renderNavBar || this.renderNavBar).bind(this, { index, viewMoment })}
/>
}
onFooterTodayClick() {
this.views[0].onFooterTodayClick()
}
onFooterClearClick() {
this.views[0].onFooterClearClick()
}
onFooterOkClick() {
this.views[0].onFooterOkClick()
}
onFooterCancelClick() {
this.views[0].onFooterCancelClick()
}
isFocused() {
const firstView = this.views[0]
if (firstView) {
return firstView.isFocused()
}
return false
}
focus() {
const firstView = this.views[0]
if (firstView) {
firstView.focus()
}
}
setHoverRange(hoverRange) {
this.setState({
hoverRange
})
}
select({ dateMoment, timestamp }) {
// if (!dateMoment) {
// return
// }
const props = this.p
const visibleRange = [props.inViewStart, props.inViewEnd]
// TODO check why this was needed
// if (!isInRange(dateMoment, { range: visibleRange, inclusive: true })) {
// return
// }
this.onAdjustViewDateChange({ dateMoment, timestamp })
this.onActiveDateChange({ dateMoment, timestamp })
const range = props.range
if (range) {
this.selectRange({ dateMoment, timestamp })
} else {
this.onChange({ dateMoment, timestamp }, event)
}
}
selectRange({ dateMoment, timestamp }) {
return MonthView.prototype.selectRange.call(this, { dateMoment, timestamp })
}
onRangeChange(range) {
return MonthView.prototype.onRangeChange.call(this, range)
}
onViewKeyDown(...args) {
const view = this.views[0]
if (view) {
view.onViewKeyDown(...args)
}
}
renderNavBar(config, navBarProps) {
return renderNavBar.call(this, config, navBarProps)
}
onMonthNavigate(index, dir, event, getNavigationDate) {
const props = this.p
event.preventDefault()
if (!props.activeDate) {
return
}
const key = event.key
const homeEndDate = key == 'Home' ? props.viewStart : props.viewEnd
const mom = key == 'Home' || key == 'End' ?
homeEndDate :
props.activeDate
const nextMoment = getNavigationDate(dir, this.toMoment(mom))
const viewMoment = this.toMoment(nextMoment)
this.onActiveDateChange({
dateMoment: nextMoment,
timestamp: +nextMoment
})
if (this.isInRange(viewMoment)) {
return
}
if (viewMoment.isAfter(props.viewEnd)) {
viewMoment.add(-props.size + 1, 'month')
}
this.onViewDateChange({
dateMoment: viewMoment,
timestamp: +viewMoment
})
}
onAdjustViewDateChange({ dateMoment, timestamp }) {
const props = this.p
let update = dateMoment == null
if (dateMoment && dateMoment.isAfter(props.viewEnd)) {
dateMoment = this.toMoment(dateMoment).add(-props.size + 1, 'month')
timestamp = +dateMoment
update = true
} else if (dateMoment && dateMoment.isBefore(props.viewStart)) {
update = true
}
if (update) {
this.onViewDateChange({ dateMoment, timestamp })
}
}
updateViewMoment(dateMoment, dir) {
const sign = dir < 0 ? -1 : 1
const abs = Math.abs(dir)
const newMoment = this.toMoment(this.p.viewStart)
newMoment.add(sign, abs == 1 ? 'month' : 'year')
return newMoment
}
renderHiddenNav(props) {
return <InlineBlock {...props} style={{ visibility: 'hidden' }} />
}
isInRange(moment) {
return isInRange(moment, [this.p.viewStart, this.p.viewEnd])
}
isInView(moment) {
return this.isInRange(moment)
}
onNavViewDateChange(dateString, { dateMoment, timestamp }) {
this.onViewDateChange({ dateMoment, timestamp })
}
onViewDateChange({ dateMoment, timestamp }) {
if (this.props.viewDate === undefined) {
this.setState({
viewDate: timestamp
})
}
if (this.props.onViewDateChange) {
const dateString = this.format(dateMoment)
this.props.onViewDateChange(dateString, { dateMoment, dateString, timestamp })
}
}
onActiveDateChange({ dateMoment, timestamp }) {
const valid = this.views.reduce((isValid, view) => {
return isValid && view.isValidActiveDate(timestamp)
}, true)
if (!valid) {
return
}
const props = this.p
const range = props.range
if (range && props.rangeStart) {
this.setState({
rangeStart: props.rangeStart,
range: clampRange([props.rangeStart, dateMoment])
})
}
if (this.props.activeDate === undefined) {
this.setState({
activeDate: timestamp
})
}
if (this.props.onActiveDateChange) {
const dateString = this.format(dateMoment)
this.props.onActiveDateChange(dateString, { dateMoment, dateString, timestamp })
}
}
gotoViewDate({ dateMoment, timestamp }) {
if (!timestamp) {
timestamp = +dateMoment
}
this.onViewDateChange({ dateMoment, timestamp })
this.onActiveDateChange({ dateMoment, timestamp })
}
format(mom) {
return mom == null ? '' : mom.format(this.props.dateFormat)
}
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, dateString, timestamp }, event)
}
}
getViewSize() {
return this.props.size
}
}
MultiMonthView.defaultProps = {
perRow: 2,
size: 2,
enableHistoryView: true,
footerClearDate: null,
isDatePicker: true,
forceViewUpdate: false,
navigation: true,
theme: 'default',
constrainActiveInView: true,
dateFormat: 'YYYY-MM-DD'
}
MultiMonthView.propTypes = {
}