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