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