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