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