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