react-date-picker
Version:
A carefully crafted date picker for React
642 lines (502 loc) • 15.7 kB
JavaScript
import React from 'react'
import { findDOMNode } from 'react-dom'
import Component from 'react-class'
import assign from 'object-assign'
import join from './join'
import toMoment from './toMoment'
import forwardTime from './utils/forwardTime'
import getTransitionEnd from './getTransitionEnd'
import assignDefined from './assignDefined'
import { renderFooter } from './MonthView'
import NavBar from './NavBar'
import { Flex } from 'react-flex'
import times from './utils/times'
import InlineBlock from 'react-inline-block'
import normalize from 'react-style-normalizer'
const renderHiddenNav = (props) => <InlineBlock {...props} style={{visibility: 'hidden'}} />
const joinFunctions = (a, b) => {
if (a && b) {
return (...args) => {
a(...args)
b(...args)
}
}
return a || b
}
const TRANSITION_DURATION = '0.4s'
export default class TransitionView extends Component {
constructor(props) {
super(props)
const child = React.Children.toArray(this.props.children)[0]
const childProps = child.props
const viewDate = props.viewDate ||
props.defaultViewDate ||
props.defaultDate ||
props.date ||
childProps.viewDate ||
childProps.defaultViewDate ||
childProps.defaultDate ||
childProps.date
const dateFormat = props.dateFormat || childProps.dateFormat
const locale = props.locale || childProps.locale
this.state = {
rendered: false,
viewDate: this.toMoment(viewDate, { dateFormat, locale })
}
}
toMoment(value, props) {
props = props || this.props
return toMoment(value, {
locale: props.locale,
dateFormat: props.dateFormat
})
}
format(mom, props) {
props = props || this.props
return mom.format(props.dateFormat)
}
componentDidMount() {
this.setState({
rendered: true
})
}
componentWillReceiveProps(nextProps) {
if (nextProps.viewDate) {
// this is in order to transition when the prop changes
// if we were to simply do setState({ viewDate }) it wouldn't have had a transition
this.transitionTo(nextProps.viewDate, nextProps)
}
}
transitionTo(date, props) {
props = props || this.props
const dateMoment = this.toMoment(date, props)
this.doTransition(dateMoment)
}
getViewChild() {
return React.Children.toArray(this.props.children)
.filter(c => c && c.props && c.props.isDatePicker)[0]
}
prepareChildProps(child, extraProps) {
if (this.view) {
return this.view.p
}
child = child || this.getViewChild()
return assign({}, child.props, extraProps)
}
render() {
const props = this.props
const child = this.child = this.getViewChild()
let viewDate = this.state.viewDate || props.viewMoment || props.viewDate
const renderedChildProps =
this.renderedChildProps =
this.prepareChildProps(child, assignDefined({
viewDate
}))
viewDate = this.state.viewDate ||
renderedChildProps.viewMoment ||
renderedChildProps.viewDate
if (!this.state.transition) {
this.viewDate = viewDate
}
const multiView = !!(child.props.size && child.props.size >= 2)
const onViewDateChange = joinFunctions(this.onViewDateChange, props.onViewDateChange)
// TODO make transition view pass all props, as is to child component
const newProps = {
key: 'picker',
ref: (v) => { this.view = v },
viewDate: this.viewDate,
onViewDateChange,
navigation: multiView,
constrainActiveInView: props.constrainActiveInView,
className: join(
child.props.className,
'react-date-picker__center'
)
}
// only pass those down if they have been specified
// as props on this TransitionView
assignDefined(newProps, {
// tabIndex: -1,
range: props.range,
date: props.date,
activeDate: props.activeDate,
footer: false,
insideField: props.insideField,
defaultRange: props.defaultRange,
defaultDate: props.defaultDate,
defaultActiveDate: props.defaultActiveDate,
// this is here in order to ensure time changes are reflected
// when using a TransitionView inside a DateField
onTimeChange: props.onTimeChange,
onClockInputBlur: props.onClockInputBlur,
onClockInputFocus: props.onClockInputFocus,
onClockEnterKey: props.onClockEnterKey,
onClockEscapeKey: props.onClockEscapeKey,
showClock: props.showClock,
tabIndex: props.tabIndex,
dateFormat: props.dateFormat,
locale: props.locale,
theme: props.theme,
minDate: props.minDate,
maxDate: props.maxDate,
onKeyDown: this.onKeyDown,
onBlur: this.onBlur
})
if (props.onChange) {
newProps.onChange = joinFunctions(props.onChange, renderedChildProps.onChange)
}
if (props.onRangeChange) {
newProps.onRangeChange = joinFunctions(props.onRangeChange, renderedChildProps.onRangeChange)
}
if (props.onActiveDateChange) {
newProps.onActiveDateChange = joinFunctions(
props.onActiveDateChange,
renderedChildProps.onActiveDateChange
)
}
if (this.state.transition) {
this.transitionDurationStyle = normalize({
transitionDuration: props.transitionDuration || TRANSITION_DURATION
})
newProps.style = assign({}, child.props.style, this.transitionDurationStyle)
newProps.className = join(
newProps.className,
'react-date-picker--transition',
`react-date-picker--transition-${this.state.transition == -1 ? 'left' : 'right'}`
)
}
let navBar
const navBarProps = {
minDate: props.minDate || renderedChildProps.minDate,
maxDate: props.maxDate || renderedChildProps.maxDate,
enableHistoryView: props.enableHistoryView === undefined ?
renderedChildProps.enableHistoryView :
props.enableHistoryView,
secondary: true,
viewDate: this.nextViewDate || this.viewDate,
onViewDateChange,
multiView
}
if (props.navigation) {
navBar = this.renderNavBar(assign({}, navBarProps, { mainNavBar: true }))
}
let footer
if (props.footer) {
footer = renderFooter(props, props.insideField ? props : this.view)
}
if (multiView) {
newProps.renderNavBar = this.renderMultiViewNavBar.bind(this, navBarProps)
}
const clone = React.cloneElement(child, newProps)
return <Flex
column
inline
wrap={false}
alignItems="stretch"
{...props}
className={join(
props.className,
'react-date-picker__transition-month-view',
props.theme && `react-date-picker__transition-month-view--theme-${props.theme}`
)}
>
{navBar}
<Flex inline row style={{ position: 'relative' }}>
{this.renderAt(-1, { multiView, navBarProps })}
{clone}
{this.renderAt(1, { multiView, navBarProps })}
</Flex>
{footer}
</Flex>
}
tryNavBarKeyDown(event) {
if (this.navBar && this.navBar.getHistoryView) {
const historyView = this.navBar.getHistoryView()
if (historyView && historyView.onKeyDown) {
historyView.onKeyDown(event)
return true
}
}
return false
}
onKeyDown(event) {
const initialKeyDown = this.child.onKeyDown
if (this.tryNavBarKeyDown(event)) {
return false
}
if (initialKeyDown) {
return initialKeyDown(event)
}
}
isHistoryViewVisible() {
if (this.navBar && this.navBar.isHistoryViewVisible) {
return this.navBar.isHistoryViewVisible()
}
return false
}
showHistoryView() {
if (this.navBar) {
this.navBar.showHistoryView()
}
}
hideHistoryView() {
if (this.navBar) {
this.navBar.hideHistoryView()
}
}
onBlur(event) {
const initialBlur = this.child.onBlur
this.hideHistoryView()
if (initialBlur) {
initialBlur(event)
}
return true
}
/**
* This method is only called when rendering the NavBar of the MonthViews
* that are not on the first row of the MultiMonthView
*
* @param {Object} navBarProps
* @param {Object} config
* @return {ReactNode}
*/
renderMultiViewNavBar(navBarProps, config) {
const { index } = config
const count = this.child.props.perRow
if (index >= count) {
const viewDate = this.toMoment(navBarProps.viewDate)
.add(index, 'month')
return <NavBar
{...navBarProps}
renderNavNext={renderHiddenNav}
renderNavPrev={renderHiddenNav}
onViewDateChange={null}
viewDate={this.toMoment(viewDate)}
/>
}
return null
}
renderNavBar(navBarProps) {
navBarProps = assign({}, navBarProps)
if (navBarProps.mainNavBar) {
navBarProps.ref = (navBar) => { this.navBar = navBar }
navBarProps.onMouseDown = this.onNavMouseDown
}
const props = this.props
const { multiView } = navBarProps
const navBar = React.Children.toArray(props.children)
.filter(c => c && c.props && c.props.isDatePickerNavBar)[0]
let newProps = navBarProps
if (navBar) {
newProps = assign({}, navBarProps, navBar.props)
// have viewDate & onViewDateChange win over initial navBar.props
newProps.viewDate = navBarProps.viewDate
newProps.onViewDateChange = navBarProps.onViewDateChange
}
if (multiView) {
const count = this.child.props.perRow
const viewSize = this.getViewSize()
const bars = times(count).map(index => {
const onUpdate = (dateMoment, dir) => {
const mom = this.toMoment(newProps.viewDate)
if (Math.abs(dir) == 1) {
mom.add(dir * viewSize, 'month')
} else {
const sign = dir > 0 ? 1 : -1
mom.add(sign, 'year')
}
return mom
}
const barProps = assign({}, newProps, {
onUpdate,
renderNavNext: renderHiddenNav,
renderNavPrev: renderHiddenNav,
viewDate: this.toMoment(newProps.viewDate).add(index, 'month')
})
if (index == 0) {
delete barProps.renderNavPrev
}
if (index == count - 1) {
delete barProps.renderNavNext
}
return <NavBar flex {...barProps} />
})
return <Flex row children={bars} />
}
return navBar ?
React.cloneElement(navBar, newProps) :
<NavBar {...newProps} />
}
getViewSize() {
return this.view && this.view.getViewSize ?
this.view.getViewSize() || 1 :
1
}
renderAt(index, { multiView, navBarProps }) {
if (!this.state.rendered || !this.view) { // || this.state.prepareTransition != -index ) {
return null
}
const viewSize = this.getViewSize()
const viewDiff = viewSize * index
const childProps = this.child.props
const renderedProps = this.renderedChildProps
let viewDate = this.toMoment(this.viewDate).add(viewDiff, 'month')
if (this.nextViewDate && this.state.prepareTransition == -index) {
// we're transitioning to this viewDate, so make sure
// it renders the date we'll need at the end of the transition
viewDate = this.nextViewDate
}
let date = renderedProps.date || renderedProps.moment
if (this.state.transitionTime) {
date = forwardTime(this.state.transitionTime, this.toMoment(date))
// console.log('date.format', date.format('HH:mm'));
}
const newProps = assign({
date,
readOnly: true,
range: renderedProps.range,
activeDate: renderedProps.activeDate,
dateFormat: renderedProps.dateFormat,
locale: renderedProps.locale,
tabIndex: -1,
clockTabIndex: -1,
navigation: multiView,
viewDate,
key: index,
footer: false,
className: join(
childProps.className,
`react-date-picker__${index == -1 ? 'prev' : 'next'}`
)
})
assignDefined(newProps, {
showClock: renderedProps.showClock,
minDate: renderedProps.minDate,
maxDate: renderedProps.maxDate
})
if (this.state.transition && this.state.transition != index) {
newProps.style = assign({}, childProps.style, this.transitionDurationStyle)
newProps.className = join(
newProps.className,
'react-date-picker--transition',
`react-date-picker--transition-${this.state.transition == -1 ? 'left' : 'right'}`
)
}
if (multiView) {
newProps.renderNavBar = this.renderMultiViewNavBar.bind(
this,
assign({}, navBarProps, { viewDate, onViewDateChange: null })
)
}
return React.cloneElement(this.child, newProps)
}
getView() {
return this.view
}
isInView(...args) {
return this.view.isInView(...args)
}
onViewDateChange(dateString, { dateMoment }) {
this.doTransition(dateMoment)
}
doTransition(dateMoment) {
if (this.state.transition) {
// this.nextViewDate = dateMoment
return
}
// to protect of null, which will default to current date
dateMoment = this.toMoment(dateMoment)
const newMoment = this.toMoment(dateMoment).startOf('month')
const viewMoment = this.toMoment(this.viewDate).startOf('month')
if (newMoment.format('YYYY-MM') == viewMoment.format('YYYY-MM')) {
return
}
const navNext = newMoment.isAfter(viewMoment)
const transition = navNext ? -1 : 1
const viewSize = this.getViewSize()
if (Math.abs(viewSize) > 1) {
const temp = this.toMoment(viewMoment).add(viewSize * -transition, 'month')
if (navNext) {
dateMoment = dateMoment.isAfter(temp) ? dateMoment : temp
} else {
dateMoment = dateMoment.isBefore(temp) ? dateMoment : temp
}
}
const transitionTime = this.props.getTransitionTime ?
this.props.getTransitionTime() :
null
this.setState({
transitionTime,
prepareTransition: transition
}, () => {
setTimeout(() => {
// in order to allow this.view.p to update
if (!findDOMNode(this.view)) {
return
}
this.nextViewDate = dateMoment
this.addTransitionEnd()
this.setState({
transition
})
})
})
}
addTransitionEnd() {
const dom = findDOMNode(this.view)
if (dom) {
dom.addEventListener(getTransitionEnd(), this.onTransitionEnd, false)
}
}
removeTransitionEnd(dom) {
dom = dom || findDOMNode(this.view)
if (dom) {
dom.removeEventListener(getTransitionEnd(), this.onTransitionEnd)
}
}
onTransitionEnd() {
this.removeTransitionEnd()
if (!this.nextViewDate) {
return
}
this.setState({
viewDate: this.nextViewDate,
transition: 0,
prepareTransition: 0
})
if (this.props.focusOnTransitionEnd) {
this.focus()
}
delete this.nextViewDate
}
onNavMouseDown() {
if (this.props.focusOnNavMouseDown && !this.isFocused()) {
this.focus()
}
}
isFocused() {
const view = this.getView()
if (view) {
return view.isFocused()
}
return false
}
focus() {
this.getView().focus()
}
}
TransitionView.propTypes = {
children: React.PropTypes.node.isRequired
}
TransitionView.defaultProps = {
focusOnNavMouseDown: true,
onTransitionStart: () => {},
onTransitionEnd: () => {},
footerClearDate: null,
enableHistoryView: true,
constrainActiveInView: false,
focusOnTransitionEnd: false,
navigation: true,
theme: 'default',
isDatePicker: true
}