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