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