'use strict'; var assign = require('object-assign'), PropTypes = require('prop-types'), createClass = require('create-react-class'), moment = require('moment'), React = require('react'), CalendarContainer = require('./src/CalendarContainer'); var TYPES = PropTypes; var Datetime = createClass({ propTypes: { // value: TYPES.object | TYPES.string, // defaultValue: TYPES.object | TYPES.string, onFocus: TYPES.func, onBlur: TYPES.func, onChange: TYPES.func, onViewModeChange: TYPES.func, locale: TYPES.string, utc: TYPES.bool, input: TYPES.bool, // dateFormat: TYPES.string | TYPES.bool, // timeFormat: TYPES.string | TYPES.bool, inputProps: TYPES.object, timeConstraints: TYPES.object, viewMode: TYPES.oneOf(['years', 'months', 'days', 'time']), isValidDate: TYPES.func, open: TYPES.bool, strictParsing: TYPES.bool, closeOnSelect: TYPES.bool, closeOnTab: TYPES.bool }, getInitialState: function() { var state = this.getStateFromProps(this.props); if (state.open === undefined) state.open = !this.props.input; state.currentView = this.props.dateFormat ? this.props.viewMode || state.updateOn || 'days' : 'time'; return state; }, getStateFromProps: function(props) { var formats = this.getFormats(props), date = props.value || props.defaultValue, selectedDate, viewDate, updateOn, inputValue; if (date && typeof date === 'string') selectedDate = this.localMoment(date, formats.datetime); else if (date) selectedDate = this.localMoment(date); if (selectedDate && !selectedDate.isValid()) selectedDate = null; viewDate = selectedDate ? selectedDate.clone().startOf('month') : this.localMoment().startOf('month'); updateOn = this.getUpdateOn(formats); if (selectedDate) inputValue = selectedDate.format(formats.datetime); else if (date.isValid && !date.isValid()) inputValue = ''; else inputValue = date || ''; return { updateOn: updateOn, inputFormat: formats.datetime, viewDate: viewDate, selectedDate: selectedDate, inputValue: inputValue, open: props.open }; }, getUpdateOn: function(formats) { if (formats.date.match(/[lLD]/)) { return 'days'; } else if (formats.date.indexOf('M') !== -1) { return 'months'; } else if (formats.date.indexOf('Y') !== -1) { return 'years'; } return 'days'; }, getFormats: function(props) { var formats = { date: props.dateFormat || '', time: props.timeFormat || '' }, locale = this.localMoment(props.date, null, props).localeData(); if (formats.date === true) { formats.date = locale.longDateFormat('L'); } else if (this.getUpdateOn(formats) !== 'days') { formats.time = ''; } if (formats.time === true) { formats.time = locale.longDateFormat('LT'); } formats.datetime = formats.date && formats.time ? formats.date + ' ' + formats.time : formats.date || formats.time; return formats; }, componentWillReceiveProps: function(nextProps) { var formats = this.getFormats(nextProps), updatedState = {}; if ( nextProps.value !== this.props.value || formats.datetime !== this.getFormats(this.props).datetime ) { updatedState = this.getStateFromProps(nextProps); } if (updatedState.open === undefined) { if (typeof nextProps.open !== 'undefined') { updatedState.open = nextProps.open; } else if ( this.props.closeOnSelect && this.state.currentView !== 'time' ) { updatedState.open = false; } else { updatedState.open = this.state.open; } } if (nextProps.viewMode !== this.props.viewMode) { updatedState.currentView = nextProps.viewMode; } if (nextProps.locale !== this.props.locale) { if (this.state.viewDate) { var updatedViewDate = this.state.viewDate .clone() .locale(nextProps.locale); updatedState.viewDate = updatedViewDate; } if (this.state.selectedDate) { var updatedSelectedDate = this.state.selectedDate .clone() .locale(nextProps.locale); updatedState.selectedDate = updatedSelectedDate; updatedState.inputValue = updatedSelectedDate.format(formats.datetime); } } if (nextProps.utc !== this.props.utc) { if (nextProps.utc) { if (this.state.viewDate) updatedState.viewDate = this.state.viewDate.clone().utc(); if (this.state.selectedDate) { updatedState.selectedDate = this.state.selectedDate.clone().utc(); updatedState.inputValue = updatedState.selectedDate.format( formats.datetime ); } } else { if (this.state.viewDate) updatedState.viewDate = this.state.viewDate.clone().local(); if (this.state.selectedDate) { updatedState.selectedDate = this.state.selectedDate.clone().local(); updatedState.inputValue = updatedState.selectedDate.format( formats.datetime ); } } } //we should only show a valid date if we are provided a isValidDate function. Removed in 2.10.3 /*if (this.props.isValidDate) { updatedState.viewDate = updatedState.viewDate || this.state.viewDate; while (!this.props.isValidDate(updatedState.viewDate)) { updatedState.viewDate = updatedState.viewDate.add(1, 'day'); } }*/ this.setState(updatedState); }, onInputChange: function(e) { var value = e.target === null ? e : e.target.value, localMoment = this.localMoment(value, this.state.inputFormat), update = { inputValue: value }; if (localMoment.isValid() && !this.props.value) { update.selectedDate = localMoment; update.viewDate = localMoment.clone().startOf('month'); } else { update.selectedDate = null; } return this.setState(update, function() { return this.props.onChange( localMoment.isValid() ? localMoment : this.state.inputValue ); }); }, onInputKey: function(e) { if (e.which === 9 && this.props.closeOnTab) { this.closeCalendar(); } }, showView: function(view) { var me = this; return function() { me.state.currentView !== view && me.props.onViewModeChange(view); me.setState({ currentView: view }); }; }, setDate: function(type) { var me = this, nextViews = { month: 'days', year: 'months' }; return function(e) { me.setState({ viewDate: me.state.viewDate .clone() [type](parseInt(e.target.getAttribute('data-value'), 10)) .startOf(type), currentView: nextViews[type] }); me.props.onViewModeChange(nextViews[type]); }; }, addTime: function(amount, type, toSelected) { return this.updateTime('add', amount, type, toSelected); }, subtractTime: function(amount, type, toSelected) { return this.updateTime('subtract', amount, type, toSelected); }, updateTime: function(op, amount, type, toSelected) { var me = this; return function() { var update = {}, date = toSelected ? 'selectedDate' : 'viewDate'; update[date] = me.state[date].clone()[op](amount, type); me.setState(update); }; }, allowedSetTime: ['hours', 'minutes', 'seconds', 'milliseconds'], setTime: function(type, value) { var index = this.allowedSetTime.indexOf(type) + 1, state = this.state, date = (state.selectedDate || state.viewDate).clone(), nextType; // It is needed to set all the time properties // to not to reset the time date[type](value); for (; index < this.allowedSetTime.length; index++) { nextType = this.allowedSetTime[index]; date[nextType](date[nextType]()); } if (!this.props.value) { this.setState({ selectedDate: date, inputValue: date.format(state.inputFormat) }); } this.props.onChange(date); }, updateSelectedDate: function(e, close) { var target = e.target, modifier = 0, viewDate = this.state.viewDate, currentDate = this.state.selectedDate || viewDate, date; if (target.className.indexOf('rdtDay') !== -1) { if (target.className.indexOf('rdtNew') !== -1) modifier = 1; else if (target.className.indexOf('rdtOld') !== -1) modifier = -1; date = viewDate .clone() .month(viewDate.month() + modifier) .date(parseInt(target.getAttribute('data-value'), 10)); } else if (target.className.indexOf('rdtMonth') !== -1) { date = viewDate .clone() .month(parseInt(target.getAttribute('data-value'), 10)) .date(currentDate.date()); } else if (target.className.indexOf('rdtYear') !== -1) { date = viewDate .clone() .month(currentDate.month()) .date(currentDate.date()) .year(parseInt(target.getAttribute('data-value'), 10)); } date .hours(currentDate.hours()) .minutes(currentDate.minutes()) .seconds(currentDate.seconds()) .milliseconds(currentDate.milliseconds()); if (!this.props.value) { var open = !(this.props.closeOnSelect && close); if (!open) { this.props.onBlur(date); } this.setState({ selectedDate: date, viewDate: date.clone().startOf('month'), inputValue: date.format(this.state.inputFormat), open: open }); } else { if (this.props.closeOnSelect && close) { this.closeCalendar(); } } this.props.onChange(date); }, openCalendar: function(e) { if (!this.state.open) { this.setState({ open: true }, function() { this.props.onFocus(e); }); } }, closeCalendar: function() { this.setState({ open: false }, function() { this.props.onBlur(this.state.selectedDate || this.state.inputValue); }); }, handleClickOutside: function() { if (this.props.input && this.state.open && !this.props.open) { this.setState({ open: false }, function() { this.props.onBlur(this.state.selectedDate || this.state.inputValue); }); } }, localMoment: function(date, format, props) { props = props || this.props; var momentFn = props.utc ? moment.utc : moment; var m = momentFn(date, format, props.strictParsing); if (props.locale) m.locale(props.locale); return m; }, componentProps: { fromProps: [ 'value', 'isValidDate', 'renderDay', 'renderMonth', 'renderYear', 'timeConstraints' ], fromState: ['viewDate', 'selectedDate', 'updateOn'], fromThis: [ 'setDate', 'setTime', 'showView', 'addTime', 'subtractTime', 'updateSelectedDate', 'localMoment', 'handleClickOutside' ] }, getComponentProps: function() { var me = this, formats = this.getFormats(this.props), props = { dateFormat: formats.date, timeFormat: formats.time }; this.componentProps.fromProps.forEach(function(name) { props[name] = me.props[name]; }); this.componentProps.fromState.forEach(function(name) { props[name] = me.state[name]; }); this.componentProps.fromThis.forEach(function(name) { props[name] = me[name]; }); return props; }, render: function() { // TODO: Make a function or clean up this code, // logic right now is really hard to follow var className = 'rdt' + (this.props.className ? Array.isArray(this.props.className) ? ' ' + this.props.className.join(' ') : ' ' + this.props.className : ''), children = []; if (this.props.input) { var finalInputProps = assign( { type: 'text', className: 'form-control', onClick: this.openCalendar, onFocus: this.openCalendar, onChange: this.onInputChange, onKeyDown: this.onInputKey, value: this.state.inputValue }, this.props.inputProps ); if (this.props.renderInput) { children = [ React.createElement( 'div', { key: 'i' }, this.props.renderInput( finalInputProps, this.openCalendar, this.closeCalendar ) ) ]; } else { children = [ React.createElement('input', assign({ key: 'i' }, finalInputProps)) ]; } } else { className += ' rdtStatic'; } if (this.state.open) className += ' rdtOpen'; return React.createElement( 'div', { className: className }, children.concat( React.createElement( 'div', { key: 'dt', className: 'rdtPicker' }, React.createElement(CalendarContainer, { view: this.state.currentView, viewProps: this.getComponentProps(), onClickOutside: this.handleClickOutside }) ) ) ); } }); Datetime.defaultProps = { className: '', defaultValue: '', inputProps: {}, input: true, onFocus: function() {}, onBlur: function() {}, onChange: function() {}, onViewModeChange: function() {}, timeFormat: true, timeConstraints: {}, dateFormat: true, strictParsing: true, closeOnSelect: false, closeOnTab: true, utc: false }; // Make moment accessible through the Datetime class Datetime.moment = moment; module.exports = Datetime;