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