Merge "Added release notes for 'ContentHandler::runLegacyHooks' removal"
[mediawiki.git] / resources / src / mediawiki.widgets.datetime / DateTimeInputWidget.js
blob3e4c5e27671cf66db04fcd3b039768d2cef765c7
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.prop( 'tabIndex', -1 );
395                                         $field.on( 'focus', this.onFieldFocus.bind( this, $field ) );
396                                 }
397                                 if ( spec.type === 'static' ) {
398                                         $field.text( spec.value );
399                                 } else {
400                                         $field.val = spanValFunc;
401                                 }
402                         }
404                         this.fields.push( $field );
405                         $field
406                                 .addClass( 'mw-widgets-datetime-dateTimeInputWidget-field' )
407                                 .data( 'mw-widgets-datetime-dateTimeInputWidget-fieldSpec', spec )
408                                 .appendTo( this.$fields );
409                 }
411                 if ( this.clearable ) {
412                         this.clearButton = new OO.ui.ButtonWidget( {
413                                 classes: [ 'mw-widgets-datetime-dateTimeInputWidget-field', 'mw-widgets-datetime-dateTimeInputWidget-clearButton' ],
414                                 framed: false,
415                                 icon: 'remove',
416                                 disabled: disabled
417                         } ).connect( this, {
418                                 click: 'onClearClick'
419                         } );
420                         this.$fields.append( this.clearButton.$element );
421                 }
423                 this.updateFieldsFromValue();
424         };
426         /**
427          * Update the UI fields from the current value
428          *
429          * @private
430          */
431         mw.widgets.datetime.DateTimeInputWidget.prototype.updateFieldsFromValue = function () {
432                 var i, $field, spec, intercalary, sz,
433                         date = this.getValueAsDate();
435                 if ( date === null ) {
436                         this.components = null;
438                         for ( i = 0; i < this.fields.length; i++ ) {
439                                 $field = this.fields[ i ];
440                                 spec = $field.data( 'mw-widgets-datetime-dateTimeInputWidget-fieldSpec' );
442                                 $field
443                                         .removeClass( 'mw-widgets-datetime-dateTimeInputWidget-invalid oo-ui-element-hidden' )
444                                         .val( '' );
446                                 if ( spec.intercalarySize ) {
447                                         if ( spec.type === 'number' ) {
448                                                 // Numbers ''should'' be the same width. But we need some extra for
449                                                 // IE, apparently.
450                                                 $field.width( ( spec.size * 1.15 ) + 'ch' );
451                                         } else {
452                                                 // Add a little for padding
453                                                 $field.width( ( spec.size * 1.15 ) + 'ch' );
454                                         }
455                                 }
456                         }
458                         this.setFlags( { invalid: this.required } );
459                 } else {
460                         this.components = this.formatter.getComponentsFromDate( date );
461                         intercalary = this.components.intercalary;
463                         for ( i = 0; i < this.fields.length; i++ ) {
464                                 $field = this.fields[ i ];
465                                 $field.removeClass( 'mw-widgets-datetime-dateTimeInputWidget-invalid' );
466                                 spec = $field.data( 'mw-widgets-datetime-dateTimeInputWidget-fieldSpec' );
467                                 if ( spec.type !== 'static' ) {
468                                         $field.val( spec.formatValue( this.components[ spec.component ] ) );
469                                 }
470                                 if ( spec.intercalarySize ) {
471                                         if ( intercalary && spec.intercalarySize[ intercalary ] !== undefined ) {
472                                                 sz = spec.intercalarySize[ intercalary ];
473                                         } else {
474                                                 sz = spec.size;
475                                         }
476                                         $field.toggleClass( 'oo-ui-element-hidden', sz <= 0 );
477                                         if ( spec.type === 'number' ) {
478                                                 // Numbers ''should'' be the same width. But we need some extra for
479                                                 // IE, apparently.
480                                                 this.fields[ i ].width( ( sz * 1.15 ) + 'ch' );
481                                         } else {
482                                                 // Add a little for padding
483                                                 this.fields[ i ].width( ( sz * 1.15 ) + 'ch' );
484                                         }
485                                 }
486                         }
488                         this.setFlags( { invalid: date < this.min || date > this.max } );
489                 }
491                 this.$element.toggleClass( 'mw-widgets-datetime-dateTimeInputWidget-empty', date === null );
492         };
494         /**
495          * Update the value with data from the UI fields
496          *
497          * @private
498          */
499         mw.widgets.datetime.DateTimeInputWidget.prototype.updateValueFromFields = function () {
500                 var i, v, $field, spec, curDate, newDate,
501                         components = {},
502                         anyInvalid = false,
503                         anyEmpty = false,
504                         allEmpty = true;
506                 for ( i = 0; i < this.fields.length; i++ ) {
507                         $field = this.fields[ i ];
508                         spec = $field.data( 'mw-widgets-datetime-dateTimeInputWidget-fieldSpec' );
509                         if ( spec.editable ) {
510                                 $field.removeClass( 'mw-widgets-datetime-dateTimeInputWidget-invalid' );
511                                 v = $field.val();
512                                 if ( v === '' ) {
513                                         $field.addClass( 'mw-widgets-datetime-dateTimeInputWidget-invalid' );
514                                         anyEmpty = true;
515                                 } else {
516                                         allEmpty = false;
517                                         v = spec.parseValue( v );
518                                         if ( v === undefined ) {
519                                                 $field.addClass( 'mw-widgets-datetime-dateTimeInputWidget-invalid' );
520                                                 anyInvalid = true;
521                                         } else {
522                                                 components[ spec.component ] = v;
523                                         }
524                                 }
525                         }
526                 }
528                 if ( allEmpty ) {
529                         for ( i = 0; i < this.fields.length; i++ ) {
530                                 this.fields[ i ].removeClass( 'mw-widgets-datetime-dateTimeInputWidget-invalid' );
531                         }
532                 } else if ( anyEmpty ) {
533                         anyInvalid = true;
534                 }
536                 if ( !anyInvalid ) {
537                         curDate = this.getValueAsDate();
538                         newDate = this.formatter.getDateFromComponents( components );
539                         if ( !curDate || !newDate || curDate.getTime() !== newDate.getTime() ) {
540                                 this.setValue( newDate );
541                         }
542                 }
543         };
545         /**
546          * Handle change event
547          *
548          * @private
549          */
550         mw.widgets.datetime.DateTimeInputWidget.prototype.onChange = function () {
551                 var date;
553                 this.updateFieldsFromValue();
555                 if ( this.calendar ) {
556                         date = this.getValueAsDate();
557                         this.calendar.setSelected( date );
558                         if ( date ) {
559                                 this.calendar.setFocusedDate( date );
560                         }
561                 }
562         };
564         /**
565          * Handle clear button click event
566          *
567          * @private
568          */
569         mw.widgets.datetime.DateTimeInputWidget.prototype.onClearClick = function () {
570                 this.blur();
571                 this.setValue( '' );
572         };
574         /**
575          * Handle click on the widget background
576          *
577          * @private
578          * @param {jQuery.Event} e Click event
579          */
580         mw.widgets.datetime.DateTimeInputWidget.prototype.onHandleClick = function () {
581                 this.focus();
582         };
584         /**
585          * Handle key down events on our field inputs.
586          *
587          * @private
588          * @param {jQuery} $field
589          * @param {jQuery.Event} e Key down event
590          * @return {boolean} False to cancel the default event
591          */
592         mw.widgets.datetime.DateTimeInputWidget.prototype.onFieldKeyDown = function ( $field, e ) {
593                 var spec = $field.data( 'mw-widgets-datetime-dateTimeInputWidget-fieldSpec' );
595                 if ( !this.isDisabled() ) {
596                         switch ( e.which ) {
597                                 case OO.ui.Keys.ENTER:
598                                 case OO.ui.Keys.SPACE:
599                                         if ( spec.type === 'boolean' ) {
600                                                 this.setValue(
601                                                         this.formatter.adjustComponent( this.getValueAsDate(), spec.component, 1, 'wrap' )
602                                                 );
603                                                 return false;
604                                         } else if ( spec.type === 'toggleLocal' ) {
605                                                 this.formatter.toggleLocal();
606                                         }
607                                         break;
609                                 case OO.ui.Keys.UP:
610                                 case OO.ui.Keys.DOWN:
611                                         if ( spec.type === 'toggleLocal' ) {
612                                                 this.formatter.toggleLocal();
613                                         } else {
614                                                 this.setValue(
615                                                         this.formatter.adjustComponent( this.getValueAsDate(), spec.component,
616                                                                 e.keyCode === OO.ui.Keys.UP ? -1 : 1, 'wrap' )
617                                                 );
618                                         }
619                                         if ( $field.is( ':input' ) ) {
620                                                 $field.select();
621                                         }
622                                         return false;
623                         }
624                 }
625         };
627         /**
628          * Handle focus events on our field inputs.
629          *
630          * @private
631          * @param {jQuery} $field
632          * @param {jQuery.Event} e Focus event
633          */
634         mw.widgets.datetime.DateTimeInputWidget.prototype.onFieldFocus = function ( $field ) {
635                 var spec = $field.data( 'mw-widgets-datetime-dateTimeInputWidget-fieldSpec' );
637                 if ( !this.isDisabled() ) {
638                         if ( this.getValueAsDate() === null ) {
639                                 this.setValue( this.formatter.getDefaultDate() );
640                         }
641                         if ( $field.is( ':input' ) ) {
642                                 $field.select();
643                         }
645                         if ( this.calendar ) {
646                                 this.calendar.toggle( !!spec.calendarComponent );
647                         }
648                 }
649         };
651         /**
652          * Handle click events on our field inputs.
653          *
654          * @private
655          * @param {jQuery} $field
656          * @param {jQuery.Event} e Click event
657          */
658         mw.widgets.datetime.DateTimeInputWidget.prototype.onFieldClick = function ( $field ) {
659                 var spec = $field.data( 'mw-widgets-datetime-dateTimeInputWidget-fieldSpec' );
661                 if ( !this.isDisabled() ) {
662                         if ( spec.type === 'boolean' ) {
663                                 this.setValue(
664                                         this.formatter.adjustComponent( this.getValueAsDate(), spec.component, 1, 'wrap' )
665                                 );
666                         } else if ( spec.type === 'toggleLocal' ) {
667                                 this.formatter.toggleLocal();
668                         }
669                 }
670         };
672         /**
673          * Handle blur events on our field inputs.
674          *
675          * @private
676          * @param {jQuery} $field
677          * @param {jQuery.Event} e Blur event
678          */
679         mw.widgets.datetime.DateTimeInputWidget.prototype.onFieldBlur = function ( $field ) {
680                 var v, date,
681                         spec = $field.data( 'mw-widgets-datetime-dateTimeInputWidget-fieldSpec' );
683                 this.updateValueFromFields();
685                 // Normalize
686                 date = this.getValueAsDate();
687                 if ( !date ) {
688                         $field.val( '' );
689                 } else {
690                         v = spec.formatValue( this.formatter.getComponentsFromDate( date )[ spec.component ] );
691                         if ( v !== $field.val() ) {
692                                 $field.val( v );
693                         }
694                 }
695         };
697         /**
698          * Handle change events on our field inputs.
699          *
700          * @private
701          * @param {jQuery} $field
702          * @param {jQuery.Event} e Change event
703          */
704         mw.widgets.datetime.DateTimeInputWidget.prototype.onFieldChange = function () {
705                 this.updateValueFromFields();
706         };
708         /**
709          * Handle wheel events on our field inputs.
710          *
711          * @private
712          * @param {jQuery} $field
713          * @param {jQuery.Event} e Change event
714          * @return {boolean} False to cancel the default event
715          */
716         mw.widgets.datetime.DateTimeInputWidget.prototype.onFieldWheel = function ( $field, e ) {
717                 var delta = 0,
718                         spec = $field.data( 'mw-widgets-datetime-dateTimeInputWidget-fieldSpec' );
720                 if ( this.isDisabled() ) {
721                         return;
722                 }
724                 // Standard 'wheel' event
725                 if ( e.originalEvent.deltaMode !== undefined ) {
726                         this.sawWheelEvent = true;
727                 }
728                 if ( e.originalEvent.deltaY ) {
729                         delta = -e.originalEvent.deltaY;
730                 } else if ( e.originalEvent.deltaX ) {
731                         delta = e.originalEvent.deltaX;
732                 }
734                 // Non-standard events
735                 if ( !this.sawWheelEvent ) {
736                         if ( e.originalEvent.wheelDeltaX ) {
737                                 delta = -e.originalEvent.wheelDeltaX;
738                         } else if ( e.originalEvent.wheelDeltaY ) {
739                                 delta = e.originalEvent.wheelDeltaY;
740                         } else if ( e.originalEvent.wheelDelta ) {
741                                 delta = e.originalEvent.wheelDelta;
742                         } else if ( e.originalEvent.detail ) {
743                                 delta = -e.originalEvent.detail;
744                         }
745                 }
747                 if ( delta && spec ) {
748                         if ( spec.type === 'toggleLocal' ) {
749                                 this.formatter.toggleLocal();
750                         } else {
751                                 this.setValue(
752                                         this.formatter.adjustComponent( this.getValueAsDate(), spec.component, delta < 0 ? -1 : 1, 'wrap' )
753                                 );
754                         }
755                         return false;
756                 }
757         };
759         /**
760          * Handle calendar change event
761          *
762          * @private
763          */
764         mw.widgets.datetime.DateTimeInputWidget.prototype.onCalendarChange = function () {
765                 var curDate = this.getValueAsDate(),
766                         newDate = this.calendar.getSelected()[ 0 ];
768                 if ( newDate ) {
769                         if ( !curDate || newDate.getTime() !== curDate.getTime() ) {
770                                 this.setValue( newDate );
771                         }
772                 }
773         };
775         /**
776          * @inheritdoc
777          * @private
778          */
779         mw.widgets.datetime.DateTimeInputWidget.prototype.getInputElement = function () {
780                 return $( '<input>' ).attr( 'type', 'hidden' );
781         };
783         /**
784          * @inheritdoc
785          */
786         mw.widgets.datetime.DateTimeInputWidget.prototype.setDisabled = function ( disabled ) {
787                 mw.widgets.datetime.DateTimeInputWidget[ 'super' ].prototype.setDisabled.call( this, disabled );
789                 // Flag all our fields as disabled
790                 if ( this.$fields ) {
791                         this.$fields.find( 'input' ).prop( 'disabled', this.isDisabled() );
792                         this.$fields.find( '[tabindex]' ).attr( 'tabindex', this.isDisabled() ? -1 : 0 );
793                 }
795                 if ( this.clearButton ) {
796                         this.clearButton.setDisabled( disabled );
797                 }
799                 return this;
800         };
802         /**
803          * @inheritdoc
804          */
805         mw.widgets.datetime.DateTimeInputWidget.prototype.focus = function () {
806                 if ( !this.$fields.find( document.activeElement ).length ) {
807                         this.$fields.find( '.mw-widgets-datetime-dateTimeInputWidget-editField' ).first().focus();
808                 }
809                 return this;
810         };
812         /**
813          * @inheritdoc
814          */
815         mw.widgets.datetime.DateTimeInputWidget.prototype.blur = function () {
816                 this.$fields.find( document.activeElement ).blur();
817                 return this;
818         };
820         /**
821          * @inheritdoc
822          */
823         mw.widgets.datetime.DateTimeInputWidget.prototype.simulateLabelClick = function () {
824                 this.focus();
825         };
827 }( jQuery, mediaWiki ) );