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.
8 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
11 * // Example of a text input widget
12 * var dateTimeInput = new mw.widgets.datetime.DateTimeInputWidget( {} )
13 * $( 'body' ).append( dateTimeInput.$element );
15 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
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
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
43 mw
.widgets
.datetime
.DateTimeInputWidget
= function MwWidgetsDatetimeDateTimeInputWidget( config
) {
44 // Configuration initialization
55 // See InputWidget#reusePreInfuseDOM about config.$input
56 if ( config
.$input
) {
57 config
.$input
.addClass( 'oo-ui-element-hidden' );
60 if ( $.isPlainObject( config
.formatter
) && config
.formatter
.format
=== undefined ) {
61 config
.formatter
.format
= '@' + config
.type
;
65 this.type
= config
.type
;
68 mw
.widgets
.datetime
.DateTimeInputWidget
.super.call( this, config
);
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
);
77 this.$handle
= $( '<span>' );
78 this.$fields
= $( '<span>' );
80 this.clearable
= !!config
.clearable
;
81 this.required
= !!config
.required
;
83 if ( typeof config
.min
=== 'string' ) {
84 config
.min
= this.parseDateValue( config
.min
);
86 if ( config
.min
instanceof Date
&& config
.min
.getTime() >= -62167219200000 ) {
87 this.min
= config
.min
;
89 this.min
= new Date( -62167219200000 ); // 0000-01-01T00:00:00.000Z
92 if ( typeof config
.max
=== 'string' ) {
93 config
.max
= this.parseDateValue( config
.max
);
95 if ( config
.max
instanceof Date
&& config
.max
.getTime() <= 253402300799999 ) {
96 this.max
= config
.max
;
98 this.max
= new Date( 253402300799999 ); // 9999-12-31T12:59:59.999Z
101 switch ( this.type
) {
103 this.min
.setUTCHours( 0, 0, 0, 0 );
104 this.max
.setUTCHours( 23, 59, 59, 999 );
107 this.min
.setUTCFullYear( 1970, 0, 1 );
108 this.max
.setUTCFullYear( 1970, 0, 1 );
111 if ( this.min
> this.max
) {
113 '"min" (' + this.min
.toISOString() + ') must not be greater than ' +
114 '"max" (' + this.max
.toISOString() + ')'
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
);
123 throw new Error( '"formatter" must be an mw.widgets.datetime.DateTimeFormatter or a plain object' );
126 if ( this.type
=== 'time' || config
.calendar
=== null ) {
127 this.calendar
= null;
129 config
.calendar
= $.extend( {}, config
.calendar
, {
130 formatter
: this.formatter
,
135 this.calendar
= new mw
.widgets
.datetime
.CalendarWidget( config
.calendar
);
140 click
: this.onHandleClick
.bind( this )
142 this.connect( this, {
145 this.formatter
.connect( this, {
148 if ( this.calendar
) {
149 this.calendar
.connect( this, {
150 change
: 'onCalendarChange'
155 this.setTabIndex( -1 );
157 this.$fields
.addClass( 'mw-widgets-datetime-dateTimeInputWidget-fields' );
161 .addClass( 'mw-widgets-datetime-dateTimeInputWidget-handle' )
162 .append( this.$icon
, this.$indicator
, this.$fields
);
165 .addClass( 'mw-widgets-datetime-dateTimeInputWidget' )
166 .append( this.$handle
);
168 if ( this.calendar
) {
169 this.$element
.append( this.calendar
.$element
);
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
);
181 /* Static properties */
183 mw
.widgets
.datetime
.DateTimeInputWidget
.static.supportsSimpleLabel
= false;
190 * Get the currently focused field, if any
195 mw
.widgets
.datetime
.DateTimeInputWidget
.prototype.getFocusedField = function () {
196 return this.$fields
.find( this.getElementDocument().activeElement
);
200 * Convert a date string to a Date
203 * @param {string} value
204 * @return {Date|null}
206 mw
.widgets
.datetime
.DateTimeInputWidget
.prototype.parseDateValue = function ( value
) {
209 value
= String( value
);
210 switch ( this.type
) {
212 value
= value
+ 'T00:00:00Z';
215 value
= '1970-01-01T' + value
+ 'Z';
218 m
= /^(\d{4,})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?Z$/.exec( value
);
221 while ( m
[ 7 ].length
< 3 ) {
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 ]
251 mw
.widgets
.datetime
.DateTimeInputWidget
.prototype.cleanUpValue = function ( value
) {
254 if ( value
=== '' ) {
258 if ( value
instanceof Date
) {
261 date
= this.parseDateValue( value
);
264 if ( date
instanceof Date
) {
265 pad = function ( v
, l
) {
267 while ( v
.length
< l
) {
273 switch ( this.type
) {
275 value
= pad( date
.getUTCFullYear(), 4 ) +
276 '-' + pad( date
.getUTCMonth() + 1, 2 ) +
277 '-' + pad( date
.getUTCDate(), 2 );
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+$/, '' );
289 value
= date
.toISOString();
300 * Get the value of the input as a Date object
302 * @return {Date|null}
304 mw
.widgets
.datetime
.DateTimeInputWidget
.prototype.getValueAsDate = function () {
305 return this.parseDateValue( this.getValue() );
309 * Set up the UI fields
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' );
320 this.data( 'mw-widgets-datetime-dateTimeInputWidget-value', v
);
322 v
= this.data( 'mw-widgets-datetime-dateTimeInputWidget-placeholder' );
328 reduceFunc = function ( k
, v
) {
329 maxlength
= Math
.max( maxlength
, v
);
331 disabled
= this.isDisabled(),
332 specs
= this.formatter
.getFieldSpec();
334 this.$fields
.empty();
335 this.clearButton
= null;
338 for ( i
= 0; i
< specs
.length
; i
++ ) {
340 if ( typeof spec
=== 'string' ) {
342 .addClass( 'mw-widgets-datetime-dateTimeInputWidget-field' )
344 .appendTo( this.$fields
);
349 while ( placeholder
.length
< spec
.size
) {
353 if ( spec
.type
=== 'number' ) {
354 // Numbers ''should'' be the same width. But we need some extra for
356 sz
= ( spec
.size
* 1.15 ) + 'ch';
358 // Add a little for padding
359 sz
= ( spec
.size
* 1.25 ) + 'ch';
361 if ( spec
.editable
&& spec
.type
!== 'static' ) {
362 if ( spec
.type
=== 'boolean' || spec
.type
=== 'toggleLocal' ) {
363 $field
= $( '<span>' )
365 tabindex
: disabled
? -1 : 0
368 .data( 'mw-widgets-datetime-dateTimeInputWidget-placeholder', placeholder
);
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
)
375 $field
.val
= spanValFunc
;
377 maxlength
= spec
.size
;
378 if ( spec
.intercalarySize
) {
379 // eslint-disable-next-line no-jquery/no-each-util
380 $.each( spec
.intercalarySize
, reduceFunc
);
382 $field
= $( '<input>' ).attr( 'type', 'text' )
384 tabindex
: disabled
? -1 : 0,
390 placeholder
: placeholder
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
)
402 $field
.addClass( 'mw-widgets-datetime-dateTimeInputWidget-editField' );
404 $field
= $( '<span>' )
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
) );
411 if ( spec
.type
=== 'static' ) {
412 $field
.text( spec
.value
);
414 $field
.val
= spanValFunc
;
418 this.fields
.push( $field
);
420 .addClass( 'mw-widgets-datetime-dateTimeInputWidget-field' )
421 .data( 'mw-widgets-datetime-dateTimeInputWidget-fieldSpec', spec
)
422 .appendTo( this.$fields
);
425 if ( this.clearable
) {
426 this.clearButton
= new OO
.ui
.ButtonWidget( {
427 classes
: [ 'mw-widgets-datetime-dateTimeInputWidget-field', 'mw-widgets-datetime-dateTimeInputWidget-clearButton' ],
432 click
: 'onClearClick'
434 this.$fields
.append( this.clearButton
.$element
);
437 this.updateFieldsFromValue();
441 * Update the UI fields from the current value
445 mw
.widgets
.datetime
.DateTimeInputWidget
.prototype.updateFieldsFromValue = function () {
446 var i
, $field
, spec
, intercalary
, sz
,
447 date
= this.getValueAsDate();
449 if ( date
=== null ) {
450 this.components
= null;
452 for ( i
= 0; i
< this.fields
.length
; i
++ ) {
453 $field
= this.fields
[ i
];
454 spec
= $field
.data( 'mw-widgets-datetime-dateTimeInputWidget-fieldSpec' );
457 .removeClass( 'mw-widgets-datetime-dateTimeInputWidget-invalid oo-ui-element-hidden' )
460 if ( spec
.intercalarySize
) {
461 if ( spec
.type
=== 'number' ) {
462 // Numbers ''should'' be the same width. But we need some extra for
464 $field
.width( ( spec
.size
* 1.15 ) + 'ch' );
466 // Add a little for padding
467 $field
.width( ( spec
.size
* 1.15 ) + 'ch' );
472 this.setFlags( { invalid
: this.required
} );
474 this.components
= this.formatter
.getComponentsFromDate( date
);
475 intercalary
= this.components
.intercalary
;
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
] ) );
484 if ( spec
.intercalarySize
) {
485 if ( intercalary
&& spec
.intercalarySize
[ intercalary
] !== undefined ) {
486 sz
= spec
.intercalarySize
[ intercalary
];
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
494 this.fields
[ i
].width( ( sz
* 1.15 ) + 'ch' );
496 // Add a little for padding
497 this.fields
[ i
].width( ( sz
* 1.15 ) + 'ch' );
502 this.setFlags( { invalid
: date
< this.min
|| date
> this.max
} );
505 this.$element
.toggleClass( 'mw-widgets-datetime-dateTimeInputWidget-empty', date
=== null );
509 * Update the value with data from the UI fields
513 mw
.widgets
.datetime
.DateTimeInputWidget
.prototype.updateValueFromFields = function () {
514 var i
, v
, $field
, spec
, curDate
, newDate
,
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' );
527 $field
.addClass( 'mw-widgets-datetime-dateTimeInputWidget-invalid' );
531 v
= spec
.parseValue( v
);
532 if ( v
=== undefined ) {
533 $field
.addClass( 'mw-widgets-datetime-dateTimeInputWidget-invalid' );
536 components
[ spec
.component
] = v
;
543 for ( i
= 0; i
< this.fields
.length
; i
++ ) {
544 this.fields
[ i
].removeClass( 'mw-widgets-datetime-dateTimeInputWidget-invalid' );
546 } else if ( anyEmpty
) {
551 curDate
= this.getValueAsDate();
552 newDate
= this.formatter
.getDateFromComponents( components
);
553 if ( !curDate
|| !newDate
|| curDate
.getTime() !== newDate
.getTime() ) {
554 this.setValue( newDate
);
560 * Handle change event
564 mw
.widgets
.datetime
.DateTimeInputWidget
.prototype.onChange = function () {
567 this.updateFieldsFromValue();
569 if ( this.calendar
) {
570 date
= this.getValueAsDate();
571 this.calendar
.setSelected( date
);
573 this.calendar
.setFocusedDate( date
);
579 * Handle clear button click event
583 mw
.widgets
.datetime
.DateTimeInputWidget
.prototype.onClearClick = function () {
589 * Handle click on the widget background
592 * @param {jQuery.Event} e Click event
594 mw
.widgets
.datetime
.DateTimeInputWidget
.prototype.onHandleClick = function () {
599 * Handle key down events on our field inputs.
602 * @param {jQuery} $field
603 * @param {jQuery.Event} e Key down event
604 * @return {boolean} False to cancel the default event
606 mw
.widgets
.datetime
.DateTimeInputWidget
.prototype.onFieldKeyDown = function ( $field
, e
) {
607 var spec
= $field
.data( 'mw-widgets-datetime-dateTimeInputWidget-fieldSpec' );
609 if ( !this.isDisabled() ) {
611 case OO
.ui
.Keys
.ENTER
:
612 case OO
.ui
.Keys
.SPACE
:
613 if ( spec
.type
=== 'boolean' ) {
615 this.formatter
.adjustComponent( this.getValueAsDate(), spec
.component
, 1, 'wrap' )
618 } else if ( spec
.type
=== 'toggleLocal' ) {
619 this.formatter
.toggleLocal();
624 case OO
.ui
.Keys
.DOWN
:
625 if ( spec
.type
=== 'toggleLocal' ) {
626 this.formatter
.toggleLocal();
629 this.formatter
.adjustComponent( this.getValueAsDate(), spec
.component
,
630 e
.keyCode
=== OO
.ui
.Keys
.UP
? -1 : 1, 'wrap' )
633 if ( $field
.is( ':input' ) ) {
634 $field
.trigger( 'select' );
642 * Handle focus events on our field inputs.
645 * @param {jQuery} $field
646 * @param {jQuery.Event} e Focus event
648 mw
.widgets
.datetime
.DateTimeInputWidget
.prototype.onFieldFocus = function ( $field
) {
649 var spec
= $field
.data( 'mw-widgets-datetime-dateTimeInputWidget-fieldSpec' );
651 if ( !this.isDisabled() ) {
652 if ( this.getValueAsDate() === null ) {
653 this.setValue( this.formatter
.getDefaultDate() );
655 if ( $field
.is( ':input' ) ) {
656 $field
.trigger( 'select' );
659 if ( this.calendar
) {
660 this.calendar
.toggle( !!spec
.calendarComponent
);
666 * Handle click events on our field inputs.
669 * @param {jQuery} $field
670 * @param {jQuery.Event} e Click event
672 mw
.widgets
.datetime
.DateTimeInputWidget
.prototype.onFieldClick = function ( $field
) {
673 var spec
= $field
.data( 'mw-widgets-datetime-dateTimeInputWidget-fieldSpec' );
675 if ( !this.isDisabled() ) {
676 if ( spec
.type
=== 'boolean' ) {
678 this.formatter
.adjustComponent( this.getValueAsDate(), spec
.component
, 1, 'wrap' )
680 } else if ( spec
.type
=== 'toggleLocal' ) {
681 this.formatter
.toggleLocal();
687 * Handle blur events on our field inputs.
690 * @param {jQuery} $field
691 * @param {jQuery.Event} e Blur event
693 mw
.widgets
.datetime
.DateTimeInputWidget
.prototype.onFieldBlur = function ( $field
) {
695 spec
= $field
.data( 'mw-widgets-datetime-dateTimeInputWidget-fieldSpec' );
697 this.updateValueFromFields();
700 date
= this.getValueAsDate();
704 v
= spec
.formatValue( this.formatter
.getComponentsFromDate( date
)[ spec
.component
] );
705 if ( v
!== $field
.val() ) {
712 * Handle change events on our field inputs.
715 * @param {jQuery} $field
716 * @param {jQuery.Event} e Change event
718 mw
.widgets
.datetime
.DateTimeInputWidget
.prototype.onFieldChange = function () {
719 this.updateValueFromFields();
723 * Handle wheel events on our field inputs.
726 * @param {jQuery} $field
727 * @param {jQuery.Event} e Change event
728 * @return {boolean} False to cancel the default event
730 mw
.widgets
.datetime
.DateTimeInputWidget
.prototype.onFieldWheel = function ( $field
, e
) {
732 spec
= $field
.data( 'mw-widgets-datetime-dateTimeInputWidget-fieldSpec' );
734 if ( this.isDisabled() || !this.getFocusedField().length
) {
738 // Standard 'wheel' event
739 if ( e
.originalEvent
.deltaMode
!== undefined ) {
740 this.sawWheelEvent
= true;
742 if ( e
.originalEvent
.deltaY
) {
743 delta
= -e
.originalEvent
.deltaY
;
744 } else if ( e
.originalEvent
.deltaX
) {
745 delta
= e
.originalEvent
.deltaX
;
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
;
761 if ( delta
&& spec
) {
762 if ( spec
.type
=== 'toggleLocal' ) {
763 this.formatter
.toggleLocal();
766 this.formatter
.adjustComponent( this.getValueAsDate(), spec
.component
, delta
< 0 ? -1 : 1, 'wrap' )
774 * Handle calendar change event
778 mw
.widgets
.datetime
.DateTimeInputWidget
.prototype.onCalendarChange = function () {
779 var curDate
= this.getValueAsDate(),
780 newDate
= this.calendar
.getSelected()[ 0 ];
783 if ( !curDate
|| newDate
.getTime() !== curDate
.getTime() ) {
784 this.setValue( newDate
);
793 mw
.widgets
.datetime
.DateTimeInputWidget
.prototype.getInputElement = function () {
794 return $( '<input>' ).attr( 'type', 'hidden' );
800 mw
.widgets
.datetime
.DateTimeInputWidget
.prototype.setDisabled = function ( disabled
) {
801 mw
.widgets
.datetime
.DateTimeInputWidget
.super.prototype.setDisabled
.call( this, disabled
);
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 );
809 if ( this.clearButton
) {
810 this.clearButton
.setDisabled( disabled
);
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' );
829 mw
.widgets
.datetime
.DateTimeInputWidget
.prototype.blur = function () {
830 this.getFocusedField().blur();
837 mw
.widgets
.datetime
.DateTimeInputWidget
.prototype.simulateLabelClick = function () {