Josh Beckman
2017-04-12 84755ceb54dd6f5fe7682912336b243e4b7dc59b
commit | author | age
c7776a 1 'use strict';
417bf4 2
c7776a 3 var assign = require('object-assign'),
84755c 4         createClass = require('create-react-class'),
9f1b61 5     moment = require('moment'),
4ad788 6     React = require('react'),
e09432 7     CalendarContainer = require('./src/CalendarContainer')
c7776a 8 ;
417bf4 9
c37f80 10 var TYPES = React.PropTypes;
84755c 11 var Datetime = createClass({
c7776a 12     propTypes: {
0d9dc7 13         // value: TYPES.object | TYPES.string,
M 14         // defaultValue: TYPES.object | TYPES.string,
aca9e6 15         onFocus: TYPES.func,
0ef08f 16         onBlur: TYPES.func,
c37f80 17         onChange: TYPES.func,
M 18         locale: TYPES.string,
049c33 19         utc: TYPES.bool,
c37f80 20         input: TYPES.bool,
cbe644 21         // dateFormat: TYPES.string | TYPES.bool,
M 22         // timeFormat: TYPES.string | TYPES.bool,
c37f80 23         inputProps: TYPES.object,
0b3475 24         timeConstraints: TYPES.object,
c37f80 25         viewMode: TYPES.oneOf(['years', 'months', 'days', 'time']),
e7f876 26         isValidDate: TYPES.func,
692390 27         open: TYPES.bool,
9012e8 28         strictParsing: TYPES.bool,
M 29         closeOnSelect: TYPES.bool,
30         closeOnTab: TYPES.bool
c7776a 31     },
8abb28 32
c7776a 33     getDefaultProps: function() {
9f1b61 34         var nof = function() {};
c7776a 35         return {
8abb28 36             className: '',
62fd2f 37             defaultValue: '',
d76f7b 38             inputProps: {},
a3a33b 39             input: true,
aca9e6 40             onFocus: nof,
4e9d38 41             onBlur: nof,
cbe644 42             onChange: nof,
839cd8 43             timeFormat: true,
0b3475 44             timeConstraints: {},
0eb226 45             dateFormat: true,
9012e8 46             strictParsing: true,
M 47             closeOnSelect: false,
049c33 48             closeOnTab: true,
TS 49             utc: false
c7776a 50         };
M 51     },
c658ad 52
c7776a 53     getInitialState: function() {
c658ad 54         var state = this.getStateFromProps( this.props );
M 55
462115 56         if ( state.open === undefined )
87c677 57             state.open = !this.props.input;
M 58
92a2c6 59         state.currentView = this.props.dateFormat ? (this.props.viewMode || state.updateOn || 'days') : 'time';
c658ad 60
M 61         return state;
62     },
63
9f1b61 64     getStateFromProps: function( props ) {
c658ad 65         var formats = this.getFormats( props ),
M 66             date = props.value || props.defaultValue,
0eb496 67             selectedDate, viewDate, updateOn, inputValue
c7776a 68         ;
3515a4 69
462115 70         if ( date && typeof date === 'string' )
c658ad 71             selectedDate = this.localMoment( date, formats.datetime );
462115 72         else if ( date )
c658ad 73             selectedDate = this.localMoment( date );
62fd2f 74
462115 75         if ( selectedDate && !selectedDate.isValid() )
62fd2f 76             selectedDate = null;
M 77
78         viewDate = selectedDate ?
462115 79             selectedDate.clone().startOf('month') :
SE 80             this.localMoment().startOf('month')
62fd2f 81         ;
3515a4 82
d1be3f 83         updateOn = this.getUpdateOn(formats);
SA 84
0eb496 85         if ( selectedDate )
SE 86             inputValue = selectedDate.format(formats.datetime);
87         else if ( date.isValid && !date.isValid() )
88             inputValue = '';
89         else
90             inputValue = date || '';
91
c7776a 92         return {
d1be3f 93             updateOn: updateOn,
c7776a 94             inputFormat: formats.datetime,
62fd2f 95             viewDate: viewDate,
0d9dc7 96             selectedDate: selectedDate,
0eb496 97             inputValue: inputValue,
50a0c2 98             open: props.open
c7776a 99         };
d1be3f 100     },
SA 101
9f1b61 102     getUpdateOn: function( formats ) {
SE 103         if ( formats.date.match(/[lLD]/) ) {
462115 104             return 'days';
d1be3f 105         }
9f1b61 106         else if ( formats.date.indexOf('M') !== -1 ) {
462115 107             return 'months';
92a2c6 108         }
9f1b61 109         else if ( formats.date.indexOf('Y') !== -1 ) {
462115 110             return 'years';
92a2c6 111         }
M 112
113         return 'days';
c7776a 114     },
aca70a 115
9f1b61 116     getFormats: function( props ) {
c7776a 117         var formats = {
839cd8 118                 date: props.dateFormat || '',
M 119                 time: props.timeFormat || ''
c37f80 120             },
64fc6a 121             locale = this.localMoment( props.date, null, props ).localeData()
c37f80 122         ;
5e870c 123
9f1b61 124         if ( formats.date === true ) {
3515a4 125             formats.date = locale.longDateFormat('L');
c7776a 126         }
9f1b61 127         else if ( this.getUpdateOn(formats) !== 'days' ) {
92a2c6 128             formats.time = '';
M 129         }
130
9f1b61 131         if ( formats.time === true ) {
3515a4 132             formats.time = locale.longDateFormat('LT');
c7776a 133         }
5e870c 134
0d9dc7 135         formats.datetime = formats.date && formats.time ?
M 136             formats.date + ' ' + formats.time :
137             formats.date || formats.time
138         ;
c7776a 139
M 140         return formats;
141     },
142
9f1b61 143     componentWillReceiveProps: function( nextProps ) {
c658ad 144         var formats = this.getFormats( nextProps ),
64fc6a 145             updatedState = {}
c37f80 146         ;
M 147
701646 148         if ( nextProps.value !== this.props.value ||
64fc6a 149             formats.datetime !== this.getFormats( this.props ).datetime ) {
SE 150             updatedState = this.getStateFromProps( nextProps );
c7776a 151         }
M 152
64fc6a 153         if ( updatedState.open === undefined ) {
fee412 154             if ( this.props.closeOnSelect && this.state.currentView !== 'time' ) {
SE 155                 updatedState.open = false;
156             } else {
157                 updatedState.open = this.state.open;
158             }
433f26 159         }
SE 160
583af6 161         if ( nextProps.viewMode !== this.props.viewMode ) {
64fc6a 162             updatedState.currentView = nextProps.viewMode;
583af6 163         }
a8a17a 164
64fc6a 165         if ( nextProps.locale !== this.props.locale ) {
SE 166             if ( this.state.viewDate ) {
167                 var updatedViewDate = this.state.viewDate.clone().locale( nextProps.locale );
168                 updatedState.viewDate = updatedViewDate;
169             }
170             if ( this.state.selectedDate ) {
171                 var updatedSelectedDate = this.state.selectedDate.clone().locale( nextProps.locale );
172                 updatedState.selectedDate = updatedSelectedDate;
173                 updatedState.inputValue = updatedSelectedDate.format( formats.datetime );
174             }
175         }
176
177         if ( nextProps.utc !== this.props.utc ) {
178             if ( nextProps.utc ) {
179                 if ( this.state.viewDate )
180                     updatedState.viewDate = this.state.viewDate.clone().utc();
181                 if ( this.state.selectedDate ) {
182                     updatedState.selectedDate = this.state.selectedDate.clone().utc();
183                     updatedState.inputValue = updatedState.selectedDate.format( formats.datetime );
184                 }
185             } else {
186                 if ( this.state.viewDate )
187                     updatedState.viewDate = this.state.viewDate.clone().local();
188                 if ( this.state.selectedDate ) {
189                     updatedState.selectedDate = this.state.selectedDate.clone().local();
190                     updatedState.inputValue = updatedState.selectedDate.format(formats.datetime);
191                 }
192             }
193         }
194
195         this.setState( updatedState );
c658ad 196     },
M 197
198     onInputChange: function( e ) {
462115 199         var value = e.target === null ? e : e.target.value,
c658ad 200             localMoment = this.localMoment( value, this.state.inputFormat ),
M 201             update = { inputValue: value }
202         ;
203
204         if ( localMoment.isValid() && !this.props.value ) {
205             update.selectedDate = localMoment;
462115 206             update.viewDate = localMoment.clone().startOf('month');
c658ad 207         }
62fd2f 208         else {
M 209             update.selectedDate = null;
210         }
c658ad 211
M 212         return this.setState( update, function() {
62fd2f 213             return this.props.onChange( localMoment.isValid() ? localMoment : this.state.inputValue );
c7776a 214         });
M 215     },
216
9f1b61 217     onInputKey: function( e ) {
SE 218         if ( e.which === 9 && this.props.closeOnTab ) {
9012e8 219             this.closeCalendar();
M 220         }
c7776a 221     },
M 222
9f1b61 223     showView: function( view ) {
c7776a 224         var me = this;
9f1b61 225         return function() {
c7776a 226             me.setState({ currentView: view });
M 227         };
228     },
229
9f1b61 230     setDate: function( type ) {
c7776a 231         var me = this,
4ad788 232             nextViews = {
M 233                 month: 'days',
234                 year: 'months'
235             }
c7776a 236         ;
9f1b61 237         return function( e ) {
c7776a 238             me.setState({
462115 239                 viewDate: me.state.viewDate.clone()[ type ]( parseInt(e.target.getAttribute('data-value'), 10) ).startOf( type ),
c7776a 240                 currentView: nextViews[ type ]
M 241             });
9fb8e8 242         };
c7776a 243     },
M 244
9f1b61 245     addTime: function( amount, type, toSelected ) {
c7776a 246         return this.updateTime( 'add', amount, type, toSelected );
M 247     },
9fb8e8 248
9f1b61 249     subtractTime: function( amount, type, toSelected ) {
c7776a 250         return this.updateTime( 'subtract', amount, type, toSelected );
M 251     },
9fb8e8 252
9f1b61 253     updateTime: function( op, amount, type, toSelected ) {
c7776a 254         var me = this;
M 255
9f1b61 256         return function() {
c7776a 257             var update = {},
M 258                 date = toSelected ? 'selectedDate' : 'viewDate'
259             ;
260
261             update[ date ] = me.state[ date ].clone()[ op ]( amount, type );
262
263             me.setState( update );
264         };
265     },
266
462115 267     allowedSetTime: ['hours', 'minutes', 'seconds', 'milliseconds'],
9f1b61 268     setTime: function( type, value ) {
c7776a 269         var index = this.allowedSetTime.indexOf( type ) + 1,
62fd2f 270             state = this.state,
M 271             date = (state.selectedDate || state.viewDate).clone(),
c7776a 272             nextType
M 273         ;
274
4ad788 275         // It is needed to set all the time properties
M 276         // to not to reset the time
c7776a 277         date[ type ]( value );
M 278         for (; index < this.allowedSetTime.length; index++) {
279             nextType = this.allowedSetTime[index];
280             date[ nextType ]( date[nextType]() );
281         }
4ad788 282
9f1b61 283         if ( !this.props.value ) {
c658ad 284             this.setState({
M 285                 selectedDate: date,
62fd2f 286                 inputValue: date.format( state.inputFormat )
c658ad 287             });
M 288         }
4e9d38 289         this.props.onChange( date );
c7776a 290     },
M 291
1fdc4e 292     updateSelectedDate: function( e, close ) {
c7776a 293         var target = e.target,
c37f80 294             modifier = 0,
62fd2f 295             viewDate = this.state.viewDate,
M 296             currentDate = this.state.selectedDate || viewDate,
c37f80 297             date
50a0c2 298     ;
c7776a 299
9f1b61 300         if (target.className.indexOf('rdtDay') !== -1) {
462115 301             if (target.className.indexOf('rdtNew') !== -1)
d1be3f 302                 modifier = 1;
462115 303             else if (target.className.indexOf('rdtOld') !== -1)
d1be3f 304                 modifier = -1;
c7776a 305
d1be3f 306             date = viewDate.clone()
SA 307                 .month( viewDate.month() + modifier )
462115 308                 .date( parseInt( target.getAttribute('data-value'), 10 ) );
9f1b61 309         } else if (target.className.indexOf('rdtMonth') !== -1) {
d1be3f 310             date = viewDate.clone()
462115 311                 .month( parseInt( target.getAttribute('data-value'), 10 ) )
SE 312                 .date( currentDate.date() );
9f1b61 313         } else if (target.className.indexOf('rdtYear') !== -1) {
d1be3f 314             date = viewDate.clone()
SA 315                 .month( currentDate.month() )
316                 .date( currentDate.date() )
462115 317                 .year( parseInt( target.getAttribute('data-value'), 10 ) );
d1be3f 318         }
SA 319
320         date.hours( currentDate.hours() )
c7776a 321             .minutes( currentDate.minutes() )
M 322             .seconds( currentDate.seconds() )
462115 323             .milliseconds( currentDate.milliseconds() );
c7776a 324
9f1b61 325         if ( !this.props.value ) {
794700 326             var open = !( this.props.closeOnSelect && close );
SE 327             if ( !open ) {
328                 this.props.onBlur( date );
329             }
330
c658ad 331             this.setState({
M 332                 selectedDate: date,
333                 viewDate: date.clone().startOf('month'),
50a0c2 334                 inputValue: date.format( this.state.inputFormat ),
794700 335                 open: open
c658ad 336             });
462115 337         } else {
794700 338             if ( this.props.closeOnSelect && close ) {
50a0c2 339                 this.closeCalendar();
M 340             }
c658ad 341         }
4e9d38 342
M 343         this.props.onChange( date );
c7776a 344     },
M 345
346     openCalendar: function() {
aca9e6 347         if (!this.state.open) {
f72983 348             this.setState({ open: true }, function() {
GV 349                 this.props.onFocus();
350             });
aca9e6 351         }
c7776a 352     },
M 353
1fdc4e 354     closeCalendar: function() {
f72983 355         this.setState({ open: false }, function () {
GV 356             this.props.onBlur( this.state.selectedDate || this.state.inputValue );
357         });
1fdc4e 358     },
EC 359
9f1b61 360     handleClickOutside: function() {
SE 361         if ( this.props.input && this.state.open && !this.props.open ) {
f72983 362             this.setState({ open: false }, function() {
GV 363                 this.props.onBlur( this.state.selectedDate || this.state.inputValue );
364             });
62fd2f 365         }
c7776a 366     },
M 367
64fc6a 368     localMoment: function( date, format, props ) {
SE 369         props = props || this.props;
370         var momentFn = props.utc ? moment.utc : moment;
371         var m = momentFn( date, format, props.strictParsing );
372         if ( props.locale )
373             m.locale( props.locale );
c37f80 374         return m;
M 375     },
376
c7776a 377     componentProps: {
0b3475 378         fromProps: ['value', 'isValidDate', 'renderDay', 'renderMonth', 'renderYear', 'timeConstraints'],
d1be3f 379         fromState: ['viewDate', 'selectedDate', 'updateOn'],
11612b 380         fromThis: ['setDate', 'setTime', 'showView', 'addTime', 'subtractTime', 'updateSelectedDate', 'localMoment', 'handleClickOutside']
c7776a 381     },
M 382
9f1b61 383     getComponentProps: function() {
c7776a 384         var me = this,
a3a33b 385             formats = this.getFormats( this.props ),
M 386             props = {dateFormat: formats.date, timeFormat: formats.time}
c7776a 387         ;
M 388
9f1b61 389         this.componentProps.fromProps.forEach( function( name ) {
c7776a 390             props[ name ] = me.props[ name ];
M 391         });
9f1b61 392         this.componentProps.fromState.forEach( function( name ) {
c7776a 393             props[ name ] = me.state[ name ];
M 394         });
9f1b61 395         this.componentProps.fromThis.forEach( function( name ) {
c7776a 396             props[ name ] = me[ name ];
M 397         });
398
399         return props;
400     },
401
402     render: function() {
e09432 403         var DOM = React.DOM,
386942 404             className = 'rdt' + (this.props.className ?
SE 405                   ( Array.isArray( this.props.className ) ?
406                   ' ' + this.props.className.join( ' ' ) : ' ' + this.props.className) : ''),
a3a33b 407             children = []
M 408         ;
409
9f1b61 410         if ( this.props.input ) {
a3a33b 411             children = [ DOM.input( assign({
18dc17 412                 key: 'i',
9f1b61 413                 type: 'text',
2bb9ca 414                 className: 'form-control',
d76f7b 415                 onFocus: this.openCalendar,
c658ad 416                 onChange: this.onInputChange,
9012e8 417                 onKeyDown: this.onInputKey,
d76f7b 418                 value: this.state.inputValue
a3a33b 419             }, this.props.inputProps ))];
462115 420         } else {
a3a33b 421             className += ' rdtStatic';
M 422         }
d76f7b 423
462115 424         if ( this.state.open )
a3a33b 425             className += ' rdtOpen';
M 426
427         return DOM.div({className: className}, children.concat(
428             DOM.div(
429                 { key: 'dt', className: 'rdtPicker' },
e09432 430                 React.createElement( CalendarContainer, {view: this.state.currentView, viewProps: this.getComponentProps(), onClickOutside: this.handleClickOutside })
d76f7b 431             )
a3a33b 432         ));
c7776a 433     }
47e834 434 });
LC 435
cc4dda 436 // Make moment accessible through the Datetime class
M 437 Datetime.moment = moment;
438
9fb8e8 439 module.exports = Datetime;