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