Avoid pointless use of isset() in LBFactoryMulti()
[mediawiki.git] / resources / src / mediawiki.widgets.datetime / DateTimeInputWidget.js
blobe9bedf5b32940e3a476f20d6f96511b01742bc79
1 ( function ( $, mw ) {
3         /**
4          * DateTimeInputWidgets can be used to input a date, a time, or a date and
5          * time, in either UTC or the user's local timezone.
6          * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
7          *
8          * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
9          *
10          *     @example
11          *     // Example of a text input widget
12          *     var dateTimeInput = new mw.widgets.datetime.DateTimeInputWidget( {} )
13          *     $( 'body' ).append( dateTimeInput.$element );
14          *
15          * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
16          *
17          * @class
18          * @extends OO.ui.InputWidget
19          * @mixins OO.ui.mixin.IconElement
20          * @mixins OO.ui.mixin.IndicatorElement
21          * @mixins OO.ui.mixin.PendingElement
22          *
23          * @constructor
24          * @param {Object} [config] Configuration options
25          * @cfg {string} [type='datetime'] Whether to act like a 'date', 'time', or 'datetime' input.
26          *  Affects values stored in the relevant <input> and the formatting and
27          *  interpretation of values passed to/from getValue() and setValue(). It's up
28          *  to the user to configure the DateTimeFormatter correctly.
29          * @cfg {Object|mw.widgets.datetime.DateTimeFormatter} [formatter={}] Configuration options for
30          *  mw.widgets.datetime.ProlepticGregorianDateTimeFormatter (with 'format' defaulting to
31          *  '@date', '@time', or '@datetime' depending on 'type'), or an
32          *  mw.widgets.datetime.DateTimeFormatter instance to use.
33          * @cfg {Object|null} [calendar={}] Configuration options for
34          *  mw.widgets.datetime.CalendarWidget; note certain settings will be forced based on the
35          *  settings passed to this widget. Set null to disable the calendar.
36          * @cfg {boolean} [required=false] Whether a value is required.
37          * @cfg {boolean} [clearable=true] Whether to provide for blanking the value.
38          * @cfg {Date|null} [value=null] Default value for the widget
39          * @cfg {Date|string|null} [min=null] Minimum allowed date
40          * @cfg {Date|string|null} [max=null] Maximum allowed date
41          */
42         mw.widgets.datetime.DateTimeInputWidget = function MwWidgetsDatetimeDateTimeInputWidget( config ) {
43                 // Configuration initialization
44                 config = $.extend( {
45                         type: 'datetime',
46                         clearable: true,
47                         required: false,
48                         min: null,
49                         max: null,
50                         formatter: {},
51                         calendar: {}
52                 }, config );
54                 // See InputWidget#reusePreInfuseDOM about config.$input
55                 if ( config.$input ) {
56                         config.$input.addClass( 'oo-ui-element-hidden' );
57                 }
59                 if ( $.isPlainObject( config.formatter ) && config.formatter.format === undefined ) {
60                         config.formatter.format = '@' + config.type;
61                 }
63                 // Early properties
64                 this.type = config.type;
66                 // Parent constructor
67                 mw.widgets.datetime.DateTimeInputWidget[ 'super' ].call( this, config );
69                 // Mixin constructors
70                 OO.ui.mixin.IconElement.call( this, config );
71                 OO.ui.mixin.IndicatorElement.call( this, config );
72                 OO.ui.mixin.PendingElement.call( this, config );
74                 // Properties
75                 this.$handle = $( '<span>' );
76                 this.$fields = $( '<span>' );
77                 this.fields = [];
78                 this.clearable = !!config.clearable;
79                 this.required = !!config.required;
81                 if ( typeof config.min === 'string' ) {
82                         config.min = this.parseDateValue( config.min );
83                 }
84                 if ( config.min instanceof Date && config.min.getTime() >= -62167219200000 ) {
85                         this.min = config.min;
86                 } else {
87                         this.min = new Date( -62167219200000 ); // 0000-01-01T00:00:00.000Z
88                 }
90                 if ( typeof config.max === 'string' ) {
91                         config.max = this.parseDateValue( config.max );
92                 }
93                 if ( config.max instanceof Date && config.max.getTime() <= 253402300799999 ) {
94                         this.max = config.max;
95                 } else {
96                         this.max = new Date( 253402300799999 ); // 9999-12-31T12:59:59.999Z
97                 }
99                 switch ( this.type ) {
100                         case 'date':
101                                 this.min.setUTCHours( 0, 0, 0, 0 );
102                                 this.max.setUTCHours( 23, 59, 59, 999 );
103                                 break;
104                         case 'time':
105                                 this.min.setUTCFullYear( 1970, 0, 1 );
106                                 this.max.setUTCFullYear( 1970, 0, 1 );
107                                 break;
108                 }
109                 if ( this.min > this.max ) {
110                         throw new Error(
111                                 '"min" (' + this.min.toISOString() + ') must not be greater than ' +
112                                 '"max" (' + this.max.toISOString() + ')'
113                         );
114                 }
116                 if ( config.formatter instanceof mw.widgets.datetime.DateTimeFormatter ) {
117                         this.formatter = config.formatter;
118                 } else if ( $.isPlainObject( config.formatter ) ) {
119                         this.formatter = new mw.widgets.datetime.ProlepticGregorianDateTimeFormatter( config.formatter );
120                 } else {
121                         throw new Error( '"formatter" must be an mw.widgets.datetime.DateTimeFormatter or a plain object' );
122                 }
124                 if ( this.type === 'time' || config.calendar === null ) {
125                         this.calendar = null;
126                 } else {
127                         config.calendar = $.extend( {}, config.calendar, {
128                                 formatter: this.formatter,
129                                 widget: this,
130                                 min: this.min,
131                                 max: this.max
132                         } );
133                         this.calendar = new mw.widgets.datetime.CalendarWidget( config.calendar );
134                 }
136                 // Events
137                 this.$handle.on( {
138                         click: this.onHandleClick.bind( this )
139                 } );
140                 this.connect( this, {
141                         change: 'onChange'
142                 } );
143                 this.formatter.connect( this, {
144                         local: 'onChange'
145                 } );
146                 if ( this.calendar ) {
147                         this.calendar.connect( this, {
148                                 change: 'onCalendarChange'
149                         } );
150                 }
152                 // Initialization
153                 this.setTabIndex( -1 );
155                 this.$fields.addClass( 'mw-widgets-datetime-dateTimeInputWidget-fields' );
156                 this.setupFields();
158                 this.$handle
159                         .addClass( 'mw-widgets-datetime-dateTimeInputWidget-handle' )
160                         .append( this.$icon, this.$indicator, this.$fields );
162                 this.$element
163                         .addClass( 'mw-widgets-datetime-dateTimeInputWidget' )
164                         .append( this.$handle );
166                 if ( this.calendar ) {
167                         this.$element.append( this.calendar.$element );
168                 }
169         };
171         /* Setup */
173         OO.inheritClass( mw.widgets.datetime.DateTimeInputWidget, OO.ui.InputWidget );
174         OO.mixinClass( mw.widgets.datetime.DateTimeInputWidget, OO.ui.mixin.IconElement );
175         OO.mixinClass( mw.widgets.datetime.DateTimeInputWidget, OO.ui.mixin.IndicatorElement );
176         OO.mixinClass( mw.widgets.datetime.DateTimeInputWidget, OO.ui.mixin.PendingElement );
178         /* Static properties */
180         mw.widgets.datetime.DateTimeInputWidget.static.supportsSimpleLabel = false;
182         /* Events */
184         /* Methods */
186         /**
187          * Convert a date string to a Date
188          *
189          * @private
190          * @param {string} value
191          * @return {Date|null}
192          */
193         mw.widgets.datetime.DateTimeInputWidget.prototype.parseDateValue = function ( value ) {
194                 var date, m;
196                 value = String( value );
197                 switch ( this.type ) {
198                         case 'date':
199                                 value = value + 'T00:00:00Z';
200                                 break;
201                         case 'time':
202                                 value = '1970-01-01T' + value + 'Z';
203                                 break;
204                 }
205                 m = /^(\d{4,})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?Z$/.exec( value );
206                 if ( m ) {
207                         if ( m[ 7 ] ) {
208                                 while ( m[ 7 ].length < 3 ) {
209                                         m[ 7 ] += '0';
210                                 }
211                         } else {
212                                 m[ 7 ] = 0;
213                         }
214                         date = new Date();
215                         date.setUTCFullYear( m[ 1 ], m[ 2 ] - 1, m[ 3 ] );
216                         date.setUTCHours( m[ 4 ], m[ 5 ], m[ 6 ], m[ 7 ] );
217                         if ( date.getTime() < -62167219200000 || date.getTime() > 253402300799999 ||
218                                 date.getUTCFullYear() !== +m[ 1 ] ||
219                                 date.getUTCMonth() + 1 !== +m[ 2 ] ||
220                                 date.getUTCDate() !== +m[ 3 ] ||
221                                 date.getUTCHours() !== +m[ 4 ] ||
222                                 date.getUTCMinutes() !== +m[ 5 ] ||
223                                 date.getUTCSeconds() !== +m[ 6 ] ||
224                                 date.getUTCMilliseconds() !== +m[ 7 ]
225                         ) {
226                                 date = null;
227                         }
228                 } else {
229                         date = null;
230                 }
232                 return date;
233         };
235         /**
236          * @inheritdoc
237          */
238         mw.widgets.datetime.DateTimeInputWidget.prototype.cleanUpValue = function ( value ) {
239                 var date, pad;
241                 if ( value === '' ) {
242                         return '';
243                 }
245                 if ( value instanceof Date ) {
246                         date = value;
247                 } else {
248                         date = this.parseDateValue( value );
249                 }
251                 if ( date instanceof Date ) {
252                         pad = function ( v, l ) {
253                                 v = String( v );
254                                 while ( v.length < l ) {
255                                         v = '0' + v;
256                                 }
257                                 return v;
258                         };
260                         switch ( this.type ) {
261                                 case 'date':
262                                         value = pad( date.getUTCFullYear(), 4 ) +
263                                                 '-' + pad( date.getUTCMonth() + 1, 2 ) +
264                                                 '-' + pad( date.getUTCDate(), 2 );
265                                         break;
267                                 case 'time':
268                                         value = pad( date.getUTCHours(), 2 ) +
269                                                 ':' + pad( date.getUTCMinutes(), 2 ) +
270                                                 ':' + pad( date.getUTCSeconds(), 2 ) +
271                                                 '.' + pad( date.getUTCMilliseconds(), 3 );
272                                         value = value.replace( /\.?0+$/, '' );
273                                         break;
275                                 default:
276                                         value = date.toISOString();
277                                         break;
278                         }
279                 } else {
280                         value = '';
281                 }
283                 return value;
284         };
286         /**
287          * Get the value of the input as a Date object
288          *
289          * @return {Date|null}
290          */
291         mw.widgets.datetime.DateTimeInputWidget.prototype.getValueAsDate = function () {
292                 return this.parseDateValue( this.getValue() );
293         };
295         /**
296          * Set up the UI fields
297          *
298          * @private
299          */
300         mw.widgets.datetime.DateTimeInputWidget.prototype.setupFields = function () {
301                 var i, $field, spec, placeholder, sz, maxlength,
302                         spanValFunc = function ( v ) {
303                                 if ( v === undefined ) {
304                                         return this.data( 'mw-widgets-datetime-dateTimeInputWidget-value' );
305                                 } else {
306                                         v = String( v );
307                                         this.data( 'mw-widgets-datetime-dateTimeInputWidget-value', v );
308                                         if ( v === '' ) {
309                                                 v = this.data( 'mw-widgets-datetime-dateTimeInputWidget-placeholder' );
310                                         }
311                                         this.text( v );
312                                         return this;
313                                 }
314                         },
315                         reduceFunc = function ( k, v ) {
316                                 maxlength = Math.max( maxlength, v );
317                         },
318                         disabled = this.isDisabled(),
319                         specs = this.formatter.getFieldSpec();
321                 this.$fields.empty();
322                 this.clearButton = null;
323                 this.fields = [];
325                 for ( i = 0; i < specs.length; i++ ) {
326                         spec = specs[ i ];
327                         if ( typeof spec === 'string' ) {
328                                 $( '<span>' )
329                                         .addClass( 'mw-widgets-datetime-dateTimeInputWidget-field' )
330                                         .text( spec )
331                                         .appendTo( this.$fields );
332                                 continue;
333                         }
335                         placeholder = '';
336                         while ( placeholder.length < spec.size ) {
337                                 placeholder += '_';
338                         }
340                         if ( spec.type === 'number' ) {
341                                 // Numbers ''should'' be the same width. But we need some extra for
342                                 // IE, apparently.
343                                 sz = ( spec.size * 1.15 ) + 'ch';
344                         } else {
345                                 // Add a little for padding
346                                 sz = ( spec.size * 1.15 ) + 'ch';
347                         }
348                         if ( spec.editable && spec.type !== 'static' ) {
349                                 if ( spec.type === 'boolean' || spec.type === 'toggleLocal' ) {
350                                         $field = $( '<span>' )
351                                                 .attr( {
352                                                         tabindex: disabled ? -1 : 0
353                                                 } )
354                                                 .width( sz )
355                                                 .data( 'mw-widgets-datetime-dateTimeInputWidget-placeholder', placeholder );
356                                         $field.on( {
357                                                 keydown: this.onFieldKeyDown.bind( this, $field ),
358                                                 focus: this.onFieldFocus.bind( this, $field ),
359                                                 click: this.onFieldClick.bind( this, $field ),
360                                                 'wheel mousewheel DOMMouseScroll': this.onFieldWheel.bind( this, $field )
361                                         } );
362                                         $field.val = spanValFunc;
363                                 } else {
364                                         maxlength = spec.size;
365                                         if ( spec.intercalarySize ) {
366                                                 $.each( spec.intercalarySize, reduceFunc );
367                                         }
368                                         $field = $( '<input>' ).attr( 'type', 'text' )
369                                                 .attr( {
370                                                         tabindex: disabled ? -1 : 0,
371                                                         size: spec.size,
372                                                         maxlength: maxlength
373                                                 } )
374                                                 .prop( {
375                                                         disabled: disabled,
376                                                         placeholder: placeholder
377                                                 } )
378                                                 .width( sz );
379                                         $field.on( {
380                                                 keydown: this.onFieldKeyDown.bind( this, $field ),
381                                                 click: this.onFieldClick.bind( this, $field ),
382                                                 focus: this.onFieldFocus.bind( this, $field ),
383                                                 blur: this.onFieldBlur.bind( this, $field ),
384                                                 change: this.onFieldChange.bind( this, $field ),
385                                                 'wheel mousewheel DOMMouseScroll': this.onFieldWheel.bind( this, $field )
386                                         } );
387                                 }
388                                 $field.addClass( 'mw-widgets-datetime-dateTimeInputWidget-editField' );
389                         } else {
390                                 $field = $( '<span>' )
391                                         .width( sz )
392                                         .data( 'mw-widgets-datetime-dateTimeInputWidget-placeholder', placeholder );
393                                 if ( spec.type === 'static' ) {
394                                         $field.text( spec.value );
395                                 } else {
396                                         $field.val = spanValFunc;
397                                 }
398                         }
400                         this.fields.push( $field );
401                         $field
402                                 .addClass( 'mw-widgets-datetime-dateTimeInputWidget-field' )
403                                 .data( 'mw-widgets-datetime-dateTimeInputWidget-fieldSpec', spec )
404                                 .appendTo( this.$fields );
405                 }
407                 if ( this.clearable ) {
408                         this.clearButton = new OO.ui.ButtonWidget( {
409                                 classes: [ 'mw-widgets-datetime-dateTimeInputWidget-field', 'mw-widgets-datetime-dateTimeInputWidget-clearButton' ],
410                                 framed: false,
411                                 icon: 'remove',
412                                 disabled: disabled
413                         } ).connect( this, {
414                                 click: 'onClearClick'
415                         } );
416                         this.$fields.append( this.clearButton.$element );
417                 }
419                 this.updateFieldsFromValue();
420         };
422         /**
423          * Update the UI fields from the current value
424          *
425          * @private
426          */
427         mw.widgets.datetime.DateTimeInputWidget.prototype.updateFieldsFromValue = function () {
428                 var i, $field, spec, intercalary, sz,
429                         date = this.getValueAsDate();
431                 if ( date === null ) {
432                         this.components = null;
434                         for ( i = 0; i < this.fields.length; i++ ) {
435                                 $field = this.fields[ i ];
436                                 spec = $field.data( 'mw-widgets-datetime-dateTimeInputWidget-fieldSpec' );
438                                 $field
439                                         .removeClass( 'mw-widgets-datetime-dateTimeInputWidget-invalid oo-ui-element-hidden' )
440                                         .val( '' );
442                                 if ( spec.intercalarySize ) {
443                                         if ( spec.type === 'number' ) {
444                                                 // Numbers ''should'' be the same width. But we need some extra for
445                                                 // IE, apparently.
446                                                 $field.width( ( spec.size * 1.15 ) + 'ch' );
447                                         } else {
448                                                 // Add a little for padding
449                                                 $field.width( ( spec.size * 1.15 ) + 'ch' );
450                                         }
451                                 }
452                         }
454                         this.setFlags( { invalid: this.required } );
455                 } else {
456                         this.components = this.formatter.getComponentsFromDate( date );
457                         intercalary = this.components.intercalary;
459                         for ( i = 0; i < this.fields.length; i++ ) {
460                                 $field = this.fields[ i ];
461                                 $field.removeClass( 'mw-widgets-datetime-dateTimeInputWidget-invalid' );
462                                 spec = $field.data( 'mw-widgets-datetime-dateTimeInputWidget-fieldSpec' );
463                                 if ( spec.type !== 'static' ) {
464                                         $field.val( spec.formatValue( this.components[ spec.component ] ) );
465                                 }
466                                 if ( spec.intercalarySize ) {
467                                         if ( intercalary && spec.intercalarySize[ intercalary ] !== undefined ) {
468                                                 sz = spec.intercalarySize[ intercalary ];
469                                         } else {
470                                                 sz = spec.size;
471                                         }
472                                         $field.toggleClass( 'oo-ui-element-hidden', sz <= 0 );
473                                         if ( spec.type === 'number' ) {
474                                                 // Numbers ''should'' be the same width. But we need some extra for
475                                                 // IE, apparently.
476                                                 this.fields[ i ].width( ( sz * 1.15 ) + 'ch' );
477                                         } else {
478                                                 // Add a little for padding
479                                                 this.fields[ i ].width( ( sz * 1.15 ) + 'ch' );
480                                         }
481                                 }
482                         }
484                         this.setFlags( { invalid: date < this.min || date > this.max } );
485                 }
487                 this.$element.toggleClass( 'mw-widgets-datetime-dateTimeInputWidget-empty', date === null );
488         };
490         /**
491          * Update the value with data from the UI fields
492          *
493          * @private
494          */
495         mw.widgets.datetime.DateTimeInputWidget.prototype.updateValueFromFields = function () {
496                 var i, v, $field, spec, curDate, newDate,
497                         components = {},
498                         anyInvalid = false,
499                         anyEmpty = false,
500                         allEmpty = true;
502                 for ( i = 0; i < this.fields.length; i++ ) {
503                         $field = this.fields[ i ];
504                         spec = $field.data( 'mw-widgets-datetime-dateTimeInputWidget-fieldSpec' );
505                         if ( spec.editable ) {
506                                 $field.removeClass( 'mw-widgets-datetime-dateTimeInputWidget-invalid' );
507                                 v = $field.val();
508                                 if ( v === '' ) {
509                                         $field.addClass( 'mw-widgets-datetime-dateTimeInputWidget-invalid' );
510                                         anyEmpty = true;
511                                 } else {
512                                         allEmpty = false;
513                                         v = spec.parseValue( v );
514                                         if ( v === undefined ) {
515                                                 $field.addClass( 'mw-widgets-datetime-dateTimeInputWidget-invalid' );
516                                                 anyInvalid = true;
517                                         } else {
518                                                 components[ spec.component ] = v;
519                                         }
520                                 }
521                         }
522                 }
524                 if ( allEmpty ) {
525                         for ( i = 0; i < this.fields.length; i++ ) {
526                                 this.fields[ i ].removeClass( 'mw-widgets-datetime-dateTimeInputWidget-invalid' );
527                         }
528                 } else if ( anyEmpty ) {
529                         anyInvalid = true;
530                 }
532                 if ( !anyInvalid ) {
533                         curDate = this.getValueAsDate();
534                         newDate = this.formatter.getDateFromComponents( components );
535                         if ( !curDate || !newDate || curDate.getTime() !== newDate.getTime() ) {
536                                 this.setValue( newDate );
537                         }
538                 }
539         };
541         /**
542          * Handle change event
543          *
544          * @private
545          */
546         mw.widgets.datetime.DateTimeInputWidget.prototype.onChange = function () {
547                 var date;
549                 this.updateFieldsFromValue();
551                 if ( this.calendar ) {
552                         date = this.getValueAsDate();
553                         this.calendar.setSelected( date );
554                         if ( date ) {
555                                 this.calendar.setFocusedDate( date );
556                         }
557                 }
558         };
560         /**
561          * Handle clear button click event
562          *
563          * @private
564          */
565         mw.widgets.datetime.DateTimeInputWidget.prototype.onClearClick = function () {
566                 this.blur();
567                 this.setValue( '' );
568         };
570         /**
571          * Handle click on the widget background
572          *
573          * @private
574          * @param {jQuery.Event} e Click event
575          */
576         mw.widgets.datetime.DateTimeInputWidget.prototype.onHandleClick = function () {
577                 this.focus();
578         };
580         /**
581          * Handle key down events on our field inputs.
582          *
583          * @private
584          * @param {jQuery} $field
585          * @param {jQuery.Event} e Key down event
586          * @return {boolean} False to cancel the default event
587          */
588         mw.widgets.datetime.DateTimeInputWidget.prototype.onFieldKeyDown = function ( $field, e ) {
589                 var spec = $field.data( 'mw-widgets-datetime-dateTimeInputWidget-fieldSpec' );
591                 if ( !this.isDisabled() ) {
592                         switch ( e.which ) {
593                                 case OO.ui.Keys.ENTER:
594                                 case OO.ui.Keys.SPACE:
595                                         if ( spec.type === 'boolean' ) {
596                                                 this.setValue(
597                                                         this.formatter.adjustComponent( this.getValueAsDate(), spec.component, 1, 'wrap' )
598                                                 );
599                                                 return false;
600                                         } else if ( spec.type === 'toggleLocal' ) {
601                                                 this.formatter.toggleLocal();
602                                         }
603                                         break;
605                                 case OO.ui.Keys.UP:
606                                 case OO.ui.Keys.DOWN:
607                                         if ( spec.type === 'toggleLocal' ) {
608                                                 this.formatter.toggleLocal();
609                                         } else {
610                                                 this.setValue(
611                                                         this.formatter.adjustComponent( this.getValueAsDate(), spec.component,
612                                                                 e.keyCode === OO.ui.Keys.UP ? -1 : 1, 'wrap' )
613                                                 );
614                                         }
615                                         if ( $field.is( ':input' ) ) {
616                                                 $field.select();
617                                         }
618                                         return false;
619                         }
620                 }
621         };
623         /**
624          * Handle focus events on our field inputs.
625          *
626          * @private
627          * @param {jQuery} $field
628          * @param {jQuery.Event} e Focus event
629          */
630         mw.widgets.datetime.DateTimeInputWidget.prototype.onFieldFocus = function ( $field ) {
631                 if ( !this.isDisabled() ) {
632                         if ( this.getValueAsDate() === null ) {
633                                 this.setValue( this.formatter.getDefaultDate() );
634                         }
635                         if ( $field.is( ':input' ) ) {
636                                 $field.select();
637                         }
639                         if ( this.calendar ) {
640                                 this.calendar.toggle( true );
641                         }
642                 }
643         };
645         /**
646          * Handle click events on our field inputs.
647          *
648          * @private
649          * @param {jQuery} $field
650          * @param {jQuery.Event} e Click event
651          */
652         mw.widgets.datetime.DateTimeInputWidget.prototype.onFieldClick = function ( $field ) {
653                 var spec = $field.data( 'mw-widgets-datetime-dateTimeInputWidget-fieldSpec' );
655                 if ( !this.isDisabled() ) {
656                         if ( spec.type === 'boolean' ) {
657                                 this.setValue(
658                                         this.formatter.adjustComponent( this.getValueAsDate(), spec.component, 1, 'wrap' )
659                                 );
660                         } else if ( spec.type === 'toggleLocal' ) {
661                                 this.formatter.toggleLocal();
662                         }
663                 }
664         };
666         /**
667          * Handle blur events on our field inputs.
668          *
669          * @private
670          * @param {jQuery} $field
671          * @param {jQuery.Event} e Blur event
672          */
673         mw.widgets.datetime.DateTimeInputWidget.prototype.onFieldBlur = function ( $field ) {
674                 var v, date,
675                         spec = $field.data( 'mw-widgets-datetime-dateTimeInputWidget-fieldSpec' );
677                 this.updateValueFromFields();
679                 // Normalize
680                 date = this.getValueAsDate();
681                 if ( !date ) {
682                         $field.val( '' );
683                 } else {
684                         v = spec.formatValue( this.formatter.getComponentsFromDate( date )[ spec.component ] );
685                         if ( v !== $field.val() ) {
686                                 $field.val( v );
687                         }
688                 }
689         };
691         /**
692          * Handle change events on our field inputs.
693          *
694          * @private
695          * @param {jQuery} $field
696          * @param {jQuery.Event} e Change event
697          */
698         mw.widgets.datetime.DateTimeInputWidget.prototype.onFieldChange = function () {
699                 this.updateValueFromFields();
700         };
702         /**
703          * Handle wheel events on our field inputs.
704          *
705          * @private
706          * @param {jQuery} $field
707          * @param {jQuery.Event} e Change event
708          * @return {boolean} False to cancel the default event
709          */
710         mw.widgets.datetime.DateTimeInputWidget.prototype.onFieldWheel = function ( $field, e ) {
711                 var delta = 0,
712                         spec = $field.data( 'mw-widgets-datetime-dateTimeInputWidget-fieldSpec' );
714                 if ( this.isDisabled() ) {
715                         return;
716                 }
718                 // Standard 'wheel' event
719                 if ( e.originalEvent.deltaMode !== undefined ) {
720                         this.sawWheelEvent = true;
721                 }
722                 if ( e.originalEvent.deltaY ) {
723                         delta = -e.originalEvent.deltaY;
724                 } else if ( e.originalEvent.deltaX ) {
725                         delta = e.originalEvent.deltaX;
726                 }
728                 // Non-standard events
729                 if ( !this.sawWheelEvent ) {
730                         if ( e.originalEvent.wheelDeltaX ) {
731                                 delta = -e.originalEvent.wheelDeltaX;
732                         } else if ( e.originalEvent.wheelDeltaY ) {
733                                 delta = e.originalEvent.wheelDeltaY;
734                         } else if ( e.originalEvent.wheelDelta ) {
735                                 delta = e.originalEvent.wheelDelta;
736                         } else if ( e.originalEvent.detail ) {
737                                 delta = -e.originalEvent.detail;
738                         }
739                 }
741                 if ( delta && spec ) {
742                         if ( spec.type === 'toggleLocal' ) {
743                                 this.formatter.toggleLocal();
744                         } else {
745                                 this.setValue(
746                                         this.formatter.adjustComponent( this.getValueAsDate(), spec.component, delta < 0 ? -1 : 1, 'wrap' )
747                                 );
748                         }
749                         return false;
750                 }
751         };
753         /**
754          * Handle calendar change event
755          *
756          * @private
757          */
758         mw.widgets.datetime.DateTimeInputWidget.prototype.onCalendarChange = function () {
759                 var curDate = this.getValueAsDate(),
760                         newDate = this.calendar.getSelected()[ 0 ];
762                 if ( newDate ) {
763                         if ( !curDate || newDate.getTime() !== curDate.getTime() ) {
764                                 this.setValue( newDate );
765                         }
766                 }
767         };
769         /**
770          * @inheritdoc
771          * @private
772          */
773         mw.widgets.datetime.DateTimeInputWidget.prototype.getInputElement = function () {
774                 return $( '<input>' ).attr( 'type', 'hidden' );
775         };
777         /**
778          * @inheritdoc
779          */
780         mw.widgets.datetime.DateTimeInputWidget.prototype.setDisabled = function ( disabled ) {
781                 mw.widgets.datetime.DateTimeInputWidget[ 'super' ].prototype.setDisabled.call( this, disabled );
783                 // Flag all our fields as disabled
784                 if ( this.$fields ) {
785                         this.$fields.find( 'input' ).prop( 'disabled', this.isDisabled() );
786                         this.$fields.find( '[tabindex]' ).attr( 'tabindex', this.isDisabled() ? -1 : 0 );
787                 }
789                 if ( this.clearButton ) {
790                         this.clearButton.setDisabled( disabled );
791                 }
793                 return this;
794         };
796         /**
797          * @inheritdoc
798          */
799         mw.widgets.datetime.DateTimeInputWidget.prototype.focus = function () {
800                 if ( !this.$fields.find( document.activeElement ).length ) {
801                         this.$fields.find( '.mw-widgets-datetime-dateTimeInputWidget-editField' ).first().focus();
802                 }
803                 return this;
804         };
806         /**
807          * @inheritdoc
808          */
809         mw.widgets.datetime.DateTimeInputWidget.prototype.blur = function () {
810                 this.$fields.find( document.activeElement ).blur();
811                 return this;
812         };
814         /**
815          * @inheritdoc
816          */
817         mw.widgets.datetime.DateTimeInputWidget.prototype.simulateLabelClick = function () {
818                 this.focus();
819         };
821 }( jQuery, mediaWiki ) );