Simon Egersand
2017-02-13 9eae924efb6dc1ba33250dcbb834ae4a7c8f4ba2
commit | author | age
c7776a 1 'use strict';
417bf4 2
c7776a 3 var assign = require('object-assign'),
9f1b61 4     moment = require('moment'),
4ad788 5     React = require('react'),
d76f7b 6     DaysView = require('./src/DaysView'),
M 7     MonthsView = require('./src/MonthsView'),
8     YearsView = require('./src/YearsView'),
9f1b61 9     TimeView = require('./src/TimeView')
c7776a 10 ;
417bf4 11
c37f80 12 var TYPES = React.PropTypes;
9fb8e8 13 var Datetime = React.createClass({
c7776a 14     mixins: [
d359eb 15         require('./src/onClickOutside')
c7776a 16     ],
M 17     viewComponents: {
18         days: DaysView,
19         months: MonthsView,
20         years: YearsView,
21         time: TimeView
22     },
23     propTypes: {
0d9dc7 24         // value: TYPES.object | TYPES.string,
M 25         // defaultValue: TYPES.object | TYPES.string,
aca9e6 26         onFocus: TYPES.func,
0ef08f 27         onBlur: TYPES.func,
c37f80 28         onChange: TYPES.func,
M 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,
c37f80 36         viewMode: TYPES.oneOf(['years', 'months', 'days', '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     getDefaultProps: function() {
9f1b61 45         var nof = function() {};
c7776a 46         return {
8abb28 47             className: '',
62fd2f 48             defaultValue: '',
d76f7b 49             inputProps: {},
a3a33b 50             input: true,
aca9e6 51             onFocus: nof,
4e9d38 52             onBlur: nof,
cbe644 53             onChange: nof,
839cd8 54             timeFormat: true,
0b3475 55             timeConstraints: {},
0eb226 56             dateFormat: true,
9012e8 57             strictParsing: true,
M 58             closeOnSelect: false,
049c33 59             closeOnTab: true,
TS 60             utc: false
c7776a 61         };
M 62     },
c658ad 63
c7776a 64     getInitialState: function() {
c658ad 65         var state = this.getStateFromProps( this.props );
M 66
462115 67         if ( state.open === undefined )
87c677 68             state.open = !this.props.input;
M 69
92a2c6 70         state.currentView = this.props.dateFormat ? (this.props.viewMode || state.updateOn || 'days') : 'time';
c658ad 71
M 72         return state;
73     },
74
9f1b61 75     getStateFromProps: function( props ) {
c658ad 76         var formats = this.getFormats( props ),
M 77             date = props.value || props.defaultValue,
0eb496 78             selectedDate, viewDate, updateOn, inputValue
c7776a 79         ;
3515a4 80
462115 81         if ( date && typeof date === 'string' )
c658ad 82             selectedDate = this.localMoment( date, formats.datetime );
462115 83         else if ( date )
c658ad 84             selectedDate = this.localMoment( date );
62fd2f 85
462115 86         if ( selectedDate && !selectedDate.isValid() )
62fd2f 87             selectedDate = null;
M 88
89         viewDate = selectedDate ?
462115 90             selectedDate.clone().startOf('month') :
SE 91             this.localMoment().startOf('month')
62fd2f 92         ;
3515a4 93
d1be3f 94         updateOn = this.getUpdateOn(formats);
SA 95
0eb496 96         if ( selectedDate )
SE 97             inputValue = selectedDate.format(formats.datetime);
98         else if ( date.isValid && !date.isValid() )
99             inputValue = '';
100         else
101             inputValue = date || '';
102
c7776a 103         return {
d1be3f 104             updateOn: updateOn,
c7776a 105             inputFormat: formats.datetime,
62fd2f 106             viewDate: viewDate,
0d9dc7 107             selectedDate: selectedDate,
0eb496 108             inputValue: inputValue,
50a0c2 109             open: props.open
c7776a 110         };
d1be3f 111     },
SA 112
9f1b61 113     getUpdateOn: function( formats ) {
SE 114         if ( formats.date.match(/[lLD]/) ) {
462115 115             return 'days';
d1be3f 116         }
9f1b61 117         else if ( formats.date.indexOf('M') !== -1 ) {
462115 118             return 'months';
92a2c6 119         }
9f1b61 120         else if ( formats.date.indexOf('Y') !== -1 ) {
462115 121             return 'years';
92a2c6 122         }
M 123
124         return 'days';
c7776a 125     },
aca70a 126
9f1b61 127     getFormats: function( props ) {
c7776a 128         var formats = {
839cd8 129                 date: props.dateFormat || '',
M 130                 time: props.timeFormat || ''
c37f80 131             },
64fc6a 132             locale = this.localMoment( props.date, null, props ).localeData()
c37f80 133         ;
5e870c 134
9f1b61 135         if ( formats.date === true ) {
3515a4 136             formats.date = locale.longDateFormat('L');
c7776a 137         }
9f1b61 138         else if ( this.getUpdateOn(formats) !== 'days' ) {
92a2c6 139             formats.time = '';
M 140         }
141
9f1b61 142         if ( formats.time === true ) {
3515a4 143             formats.time = locale.longDateFormat('LT');
c7776a 144         }
5e870c 145
0d9dc7 146         formats.datetime = formats.date && formats.time ?
M 147             formats.date + ' ' + formats.time :
148             formats.date || formats.time
149         ;
c7776a 150
M 151         return formats;
152     },
153
9f1b61 154     componentWillReceiveProps: function( nextProps ) {
c658ad 155         var formats = this.getFormats( nextProps ),
64fc6a 156             updatedState = {}
c37f80 157         ;
M 158
701646 159         if ( nextProps.value !== this.props.value ||
64fc6a 160             formats.datetime !== this.getFormats( this.props ).datetime ) {
SE 161             updatedState = this.getStateFromProps( nextProps );
c7776a 162         }
M 163
64fc6a 164         if ( updatedState.open === undefined ) {
fee412 165             if ( this.props.closeOnSelect && this.state.currentView !== 'time' ) {
SE 166                 updatedState.open = false;
167             } else {
168                 updatedState.open = this.state.open;
169             }
433f26 170         }
SE 171
583af6 172         if ( nextProps.viewMode !== this.props.viewMode ) {
64fc6a 173             updatedState.currentView = nextProps.viewMode;
583af6 174         }
a8a17a 175
64fc6a 176         if ( nextProps.locale !== this.props.locale ) {
SE 177             if ( this.state.viewDate ) {
178                 var updatedViewDate = this.state.viewDate.clone().locale( nextProps.locale );
179                 updatedState.viewDate = updatedViewDate;
180             }
181             if ( this.state.selectedDate ) {
182                 var updatedSelectedDate = this.state.selectedDate.clone().locale( nextProps.locale );
183                 updatedState.selectedDate = updatedSelectedDate;
184                 updatedState.inputValue = updatedSelectedDate.format( formats.datetime );
185             }
186         }
187
188         if ( nextProps.utc !== this.props.utc ) {
189             if ( nextProps.utc ) {
190                 if ( this.state.viewDate )
191                     updatedState.viewDate = this.state.viewDate.clone().utc();
192                 if ( this.state.selectedDate ) {
193                     updatedState.selectedDate = this.state.selectedDate.clone().utc();
194                     updatedState.inputValue = updatedState.selectedDate.format( formats.datetime );
195                 }
196             } else {
197                 if ( this.state.viewDate )
198                     updatedState.viewDate = this.state.viewDate.clone().local();
199                 if ( this.state.selectedDate ) {
200                     updatedState.selectedDate = this.state.selectedDate.clone().local();
201                     updatedState.inputValue = updatedState.selectedDate.format(formats.datetime);
202                 }
203             }
204         }
205
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 }
213         ;
214
215         if ( localMoment.isValid() && !this.props.value ) {
216             update.selectedDate = localMoment;
462115 217             update.viewDate = localMoment.clone().startOf('month');
c658ad 218         }
62fd2f 219         else {
M 220             update.selectedDate = null;
221         }
c658ad 222
M 223         return this.setState( update, function() {
62fd2f 224             return this.props.onChange( localMoment.isValid() ? localMoment : this.state.inputValue );
c7776a 225         });
M 226     },
227
9f1b61 228     onInputKey: function( e ) {
SE 229         if ( e.which === 9 && this.props.closeOnTab ) {
9012e8 230             this.closeCalendar();
M 231         }
c7776a 232     },
M 233
9f1b61 234     showView: function( view ) {
c7776a 235         var me = this;
9f1b61 236         return function() {
c7776a 237             me.setState({ currentView: view });
M 238         };
239     },
240
9f1b61 241     setDate: function( type ) {
c7776a 242         var me = this,
4ad788 243             nextViews = {
M 244                 month: 'days',
245                 year: 'months'
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             });
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
M 284         ;
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
50a0c2 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
357     openCalendar: function() {
aca9e6 358         if (!this.state.open) {
f72983 359             this.setState({ open: true }, function() {
GV 360                 this.props.onFocus();
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() {
SE 372         if ( this.props.input && this.state.open && !this.props.open ) {
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'],
c658ad 391         fromThis: ['setDate', 'setTime', 'showView', 'addTime', 'subtractTime', 'updateSelectedDate', 'localMoment']
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}
c7776a 398         ;
M 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() {
d76f7b 414         var Component = this.viewComponents[ this.state.currentView ],
a3a33b 415             DOM = React.DOM,
386942 416             className = 'rdt' + (this.props.className ?
SE 417                   ( Array.isArray( this.props.className ) ?
418                   ' ' + this.props.className.join( ' ' ) : ' ' + this.props.className) : ''),
a3a33b 419             children = []
M 420         ;
421
9f1b61 422         if ( this.props.input ) {
a3a33b 423             children = [ DOM.input( assign({
18dc17 424                 key: 'i',
9f1b61 425                 type: 'text',
2bb9ca 426                 className: 'form-control',
d76f7b 427                 onFocus: this.openCalendar,
c658ad 428                 onChange: this.onInputChange,
9012e8 429                 onKeyDown: this.onInputKey,
d76f7b 430                 value: this.state.inputValue
a3a33b 431             }, this.props.inputProps ))];
462115 432         } else {
a3a33b 433             className += ' rdtStatic';
M 434         }
d76f7b 435
462115 436         if ( this.state.open )
a3a33b 437             className += ' rdtOpen';
M 438
439         return DOM.div({className: className}, children.concat(
440             DOM.div(
441                 { key: 'dt', className: 'rdtPicker' },
442                 React.createElement( Component, this.getComponentProps())
d76f7b 443             )
a3a33b 444         ));
c7776a 445     }
47e834 446 });
LC 447
cc4dda 448 // Make moment accessible through the Datetime class
M 449 Datetime.moment = moment;
450
9fb8e8 451 module.exports = Datetime;