Merge "Bump wikimedia/parsoid to 0.21.0-a11"
[mediawiki.git] / resources / src / mediawiki.widgets / mw.widgets.DateInputWidget.js
blob10e5316163312fc0d1460a0513c646e8a11480dc
1 /*!
2  * MediaWiki Widgets – DateInputWidget class.
3  *
4  * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
5  * @license The MIT License (MIT); see LICENSE.txt
6  */
7 /* global moment */
8 ( function () {
10         /**
11          * @classdesc Date input widget.
12          *
13          * @example
14          * // Date input widget showcase
15          * let fieldset = new OO.ui.FieldsetLayout( {
16          *   items: [
17          *     new OO.ui.FieldLayout(
18          *       new mw.widgets.DateInputWidget(),
19          *       {
20          *         align: 'top',
21          *         label: 'Select date'
22          *       }
23          *     ),
24          *     new OO.ui.FieldLayout(
25          *       new mw.widgets.DateInputWidget( { precision: 'month' } ),
26          *       {
27          *         align: 'top',
28          *         label: 'Select month'
29          *       }
30          *     ),
31          *     new OO.ui.FieldLayout(
32          *       new mw.widgets.DateInputWidget( {
33          *         inputFormat: 'DD.MM.YYYY',
34          *         displayFormat: 'Do [of] MMMM [anno Domini] YYYY'
35          *       } ),
36          *       {
37          *         align: 'top',
38          *         label: 'Select date (custom formats)'
39          *       }
40          *     )
41          *   ]
42          * } );
43          * $( document.body ).append( fieldset.$element );
44          *
45          * // The value is stored in 'YYYY-MM-DD' or 'YYYY-MM' format
46          *
47          * @example
48          * // Accessing values in a date input widget
49          * let dateInput = new mw.widgets.DateInputWidget();
50          * let $label = $( '<p>' );
51          * $( document.body ).append( $label, dateInput.$element );
52          * dateInput.on( 'change', function () {
53          *   // The value will always be a valid date or empty string, malformed input is ignored
54          *   let date = dateInput.getValue();
55          *   $label.text( 'Selected date: ' + ( date || '(none)' ) );
56          * } );
57          *
58          * @class
59          * @extends OO.ui.TextInputWidget
60          * @mixes OO.ui.mixin.IndicatorElement
61          *
62          * @constructor
63          * @description Create an mw.widgets.DateInputWidget object.
64          * @param {Object} [config] Configuration options
65          * @param {string} [config.precision='day'] Date precision to use, 'day' or 'month'
66          * @param {string} [config.value] Day or month date (depending on `precision`), in the format 'YYYY-MM-DD'
67          *     or 'YYYY-MM'. If not given or empty string, no date is selected.
68          * @param {string} [config.inputFormat] Date format string to use for the textual input field. Displayed
69          *     while the widget is active, and the user can type in a date in this format. Should be short
70          *     and easy to type. When not given, defaults to 'YYYY-MM-DD' or 'YYYY-MM', depending on
71          *     `precision`.
72          * @param {string} [config.displayFormat] Date format string to use for the clickable label. Displayed
73          *     while the widget is inactive. Should be as unambiguous as possible (for example, prefer to
74          *     spell out the month, rather than rely on the order), even if that makes it longer. When not
75          *     given, the default is language-specific.
76          * @param {boolean} [config.longDisplayFormat=false] If a custom displayFormat is not specified, use
77          *     unabbreviated day of the week and month names in the default language-specific displayFormat.
78          * @param {string} [config.placeholderLabel=No date selected] Placeholder text shown when the widget is not
79          *     selected. Default text taken from message `mw-widgets-dateinput-no-date`.
80          * @param {string} [config.placeholderDateFormat] User-visible date format string displayed in the textual input
81          *     field when it's empty. Should be the same as `inputFormat`, but translated to the user's
82          *     language. When not given, defaults to a translated version of 'YYYY-MM-DD' or 'YYYY-MM',
83          *     depending on `precision`.
84          * @param {boolean} [config.required=false] Mark the field as required. Implies `indicator: 'required'`.
85          * @param {string} [config.mustBeAfter] Validates the date to be after this. In the 'YYYY-MM-DD' format.
86          * @param {string} [config.mustBeBefore] Validates the date to be before this. In the 'YYYY-MM-DD' format.
87          * @param {jQuery} [config.$overlay] Render the calendar into a separate layer. This configuration is
88          *     useful in cases where the expanded calendar is larger than its container. The specified
89          *     overlay layer is usually on top of the container and has a larger area. By default, the
90          *     calendar uses relative positioning.
91          * @param {Object} [config.calendar] Configuration options for the this input's
92          *     {@link mw.widgets.CalendarWidget CalendarWidget}.
93          */
94         mw.widgets.DateInputWidget = function MWWDateInputWidget( config ) {
95                 // Config initialization
96                 config = Object.assign( {
97                         precision: 'day',
98                         longDisplayFormat: false,
99                         required: false,
100                         placeholderLabel: mw.msg( 'mw-widgets-dateinput-no-date' )
101                 }, config );
102                 if ( config.required ) {
103                         if ( config.indicator === undefined ) {
104                                 config.indicator = 'required';
105                         }
106                 }
108                 let placeholderDateFormat;
109                 if ( config.placeholderDateFormat ) {
110                         placeholderDateFormat = config.placeholderDateFormat;
111                 } else if ( config.inputFormat ) {
112                         // We have no way to display a translated placeholder for custom formats
113                         placeholderDateFormat = '';
114                 } else {
115                         // The following messages are used here:
116                         // * mw-widgets-dateinput-placeholder-day
117                         // * mw-widgets-dateinput-placeholder-month
118                         placeholderDateFormat = mw.msg( 'mw-widgets-dateinput-placeholder-' + config.precision );
119                 }
121                 // Properties (must be set before parent constructor, which calls #setValue)
122                 this.$handle = $( '<div>' );
123                 this.innerLabel = new OO.ui.LabelWidget();
124                 this.textInput = new OO.ui.TextInputWidget( {
125                         required: config.required,
126                         placeholder: placeholderDateFormat,
127                         validate: this.validateDate.bind( this )
128                 } );
129                 this.calendar = new mw.widgets.CalendarWidget( Object.assign( {
130                         lazyInitOnToggle: true,
131                         // Can't pass `$floatableContainer: this.$element` here, the latter is not set yet.
132                         // Instead we call setFloatableContainer() below.
133                         precision: config.precision
134                 }, config.calendar ) );
135                 this.inCalendar = 0;
136                 this.inTextInput = 0;
137                 this.closing = false;
138                 this.inputFormat = config.inputFormat;
139                 this.displayFormat = config.displayFormat;
140                 this.longDisplayFormat = config.longDisplayFormat;
141                 this.required = config.required;
142                 this.placeholderLabel = config.placeholderLabel;
143                 // Validate and set min and max dates as properties
145                 if ( config.mustBeAfter !== undefined ) {
146                         const mustBeAfter = moment( config.mustBeAfter, 'YYYY-MM-DD' );
147                         if ( mustBeAfter.isValid() ) {
148                                 this.mustBeAfter = mustBeAfter;
149                         }
150                 }
151                 if ( config.mustBeBefore !== undefined ) {
152                         const mustBeBefore = moment( config.mustBeBefore, 'YYYY-MM-DD' );
153                         if ( mustBeBefore.isValid() ) {
154                                 this.mustBeBefore = mustBeBefore;
155                         }
156                 }
157                 // Parent constructor
158                 mw.widgets.DateInputWidget.super.call( this, config );
160                 // Mixin constructors
161                 OO.ui.mixin.IndicatorElement.call( this, config );
163                 // Events
164                 this.calendar.connect( this, {
165                         change: 'onCalendarChange'
166                 } );
167                 this.textInput.connect( this, {
168                         enter: 'onEnter',
169                         change: 'onTextInputChange'
170                 } );
171                 this.$element.on( {
172                         focusout: this.onBlur.bind( this )
173                 } );
174                 this.calendar.$element.on( {
175                         focusout: this.onBlur.bind( this ),
176                         click: this.onCalendarClick.bind( this ),
177                         keypress: this.onCalendarKeyPress.bind( this )
178                 } );
179                 this.$handle.on( {
180                         click: this.onClick.bind( this ),
181                         keypress: this.onKeyPress.bind( this ),
182                         focus: this.onFocus.bind( this )
183                 } );
185                 // Initialization
186                 // Move 'tabindex' from this.$input (which is invisible) to the visible handle
187                 if ( !this.isDisabled() ) {
188                         this.setTabIndexedElement( this.$handle );
189                 }
190                 this.$handle
191                         .append( this.innerLabel.$element, this.$indicator )
192                         .addClass( 'mw-widget-dateInputWidget-handle' );
193                 this.calendar.$element
194                         .addClass( 'mw-widget-dateInputWidget-calendar' );
195                 this.$element
196                         .addClass( 'mw-widget-dateInputWidget' )
197                         .append( this.$handle, this.textInput.$element, this.calendar.$element );
199                 const $overlay = config.$overlay === true ? OO.ui.getDefaultOverlay() : config.$overlay;
201                 if ( $overlay ) {
202                         this.calendar.setFloatableContainer( this.$element );
203                         $overlay.append( this.calendar.$element );
205                         // The text input and calendar are not in DOM order, so fix up focus transitions.
206                         this.textInput.$input.on( 'keydown', ( e ) => {
207                                 if ( e.which === OO.ui.Keys.TAB ) {
208                                         if ( e.shiftKey ) {
209                                                 // Tabbing backward from text input: normal browser behavior
210                                         } else {
211                                                 // Tabbing forward from text input: just focus the calendar
212                                                 this.calendar.$element.trigger( 'focus' );
213                                                 return false;
214                                         }
215                                 }
216                         } );
217                         this.calendar.$element.on( 'keydown', ( e ) => {
218                                 if ( e.which === OO.ui.Keys.TAB ) {
219                                         if ( e.shiftKey ) {
220                                                 // Tabbing backward from calendar: just focus the text input
221                                                 this.textInput.$input.trigger( 'focus' );
222                                                 return false;
223                                         } else {
224                                                 // Tabbing forward from calendar: focus the text input, then allow normal browser
225                                                 // behavior to move focus to next focusable after it
226                                                 this.textInput.$input.trigger( 'focus' );
227                                         }
228                                 }
229                         } );
230                 }
232                 // Set handle label and hide stuff
233                 this.updateUI();
234                 this.textInput.toggle( false );
235                 this.calendar.toggle( false );
237                 // See InputWidget#reusePreInfuseDOM about config.$input
238                 if ( config.$input ) {
239                         // Hide unused <input> from PHP after infusion is done
240                         config.$input.addClass( 'oo-ui-element-hidden' );
241                 }
242         };
244         /* Inheritance */
246         OO.inheritClass( mw.widgets.DateInputWidget, OO.ui.TextInputWidget );
247         OO.mixinClass( mw.widgets.DateInputWidget, OO.ui.mixin.IndicatorElement );
249         /* Events */
251         /**
252          * Fired when the widget is deactivated (when the calendar is closed). This can happen because
253          * the user selected a value, or because the user blurred the widget.
254          *
255          * @event mw.widgets.DateInputWidget.deactivate
256          * @param {boolean} userSelected Whether the deactivation happened because the user selected a value
257          */
259         /* Static Methods */
261         /**
262          * @inheritdoc
263          */
264         mw.widgets.DateInputWidget.static.reusePreInfuseDOM = function ( node, config ) {
265                 config = mw.widgets.DateInputWidget.super.static.reusePreInfuseDOM( node, config );
266                 if ( config.$input ) {
267                         // Ignore the extra field from PendingTextInputWidget (T382344)
268                         config.$input = config.$input.first();
269                 }
270                 return config;
271         };
273         /* Methods */
275         /**
276          * @inheritdoc
277          * @protected
278          */
279         mw.widgets.DateInputWidget.prototype.getInputElement = function () {
280                 return $( '<input>' ).attr( 'type', 'hidden' );
281         };
283         /**
284          * Respond to calendar date change events.
285          *
286          * @private
287          */
288         mw.widgets.DateInputWidget.prototype.onCalendarChange = function () {
289                 this.inCalendar++;
290                 if ( !this.inTextInput ) {
291                         // If this is caused by user typing in the input field, do not set anything.
292                         // The value may be invalid (see #onTextInputChange), but displayable on the calendar.
293                         this.setValue( this.calendar.getDate() );
294                 }
295                 this.inCalendar--;
296         };
298         /**
299          * Respond to text input value change events.
300          *
301          * @private
302          */
303         mw.widgets.DateInputWidget.prototype.onTextInputChange = function () {
304                 const
305                         value = this.textInput.getValue(),
306                         mom = moment( value, this.getInputFormat() ),
307                         valid = this.isValidDate( value );
308                 this.inTextInput++;
310                 if ( value === '' ) {
311                         // No date selected
312                         this.setValue( '' );
313                 } else if ( valid ) {
314                         // Well-formed date value, parse and set it
315                         // Use English locale to avoid number formatting
316                         this.setValue( mom.locale( 'en' ).format( this.getInternalFormat() ) );
317                 } else {
318                         // Not well-formed, but possibly partial? Try updating the calendar, but do not set the
319                         // internal value. Generally this only makes sense when 'inputFormat' is little-endian (e.g.
320                         // 'YYYY-MM-DD'), but that's hard to check for, and might be difficult to handle the parsing
321                         // right for weird formats. So limit this trick to only when we're using the default
322                         // 'inputFormat', which is the same as the internal format, 'YYYY-MM-DD'.
323                         if ( this.getInputFormat() === this.getInternalFormat() ) {
324                                 this.calendar.setMoment( mom );
325                         }
326                 }
327                 this.inTextInput--;
329         };
331         /**
332          * @inheritdoc
333          */
334         mw.widgets.DateInputWidget.prototype.setValue = function ( value ) {
335                 const oldValue = this.value;
337                 if ( !moment( value, this.getInternalFormat() ).isValid() ) {
338                         value = '';
339                 }
341                 mw.widgets.DateInputWidget.super.prototype.setValue.call( this, value );
343                 if ( this.value !== oldValue ) {
344                         this.updateUI();
345                         this.setValidityFlag();
346                 }
348                 return this;
349         };
351         /**
352          * Handle text input and calendar blur events.
353          *
354          * @private
355          */
356         mw.widgets.DateInputWidget.prototype.onBlur = function () {
357                 setTimeout( () => {
358                         const $focussed = $( ':focus' );
359                         // Deactivate unless the focus moved to something else inside this widget
360                         if (
361                                 !OO.ui.contains( this.$element[ 0 ], $focussed[ 0 ], true ) &&
362                                 // Calendar might be in an $overlay
363                                 !OO.ui.contains( this.calendar.$element[ 0 ], $focussed[ 0 ], true )
364                         ) {
365                                 this.deactivate();
366                         }
367                 }, 0 );
368         };
370         /**
371          * @inheritdoc
372          */
373         mw.widgets.DateInputWidget.prototype.focus = function () {
374                 this.activate();
375                 return this;
376         };
378         /**
379          * @inheritdoc
380          */
381         mw.widgets.DateInputWidget.prototype.blur = function () {
382                 this.deactivate();
383                 return this;
384         };
386         /**
387          * Update the contents of the label, text input and status of calendar to reflect selected value.
388          *
389          * @private
390          */
391         mw.widgets.DateInputWidget.prototype.updateUI = function () {
392                 if ( this.getValue() === '' ) {
393                         this.textInput.setValue( '' );
394                         this.calendar.setDate( null );
395                         this.innerLabel.setLabel( this.placeholderLabel );
396                         this.$element.addClass( 'mw-widget-dateInputWidget-empty' );
397                 } else {
398                         const moment = this.getMoment();
399                         if ( !this.inTextInput ) {
400                                 this.textInput.setValue( moment.format( this.getInputFormat() ) );
401                         }
402                         if ( !this.inCalendar ) {
403                                 this.calendar.setDate( this.getValue() );
404                         }
405                         this.innerLabel.setLabel( moment.format( this.getDisplayFormat() ) );
406                         this.$element.removeClass( 'mw-widget-dateInputWidget-empty' );
407                 }
408         };
410         /**
411          * Deactivate this input field for data entry. Closes the calendar and hides the text field.
412          *
413          * @private
414          * @param {boolean} [userSelected] Whether we are deactivating because the user selected a value
415          */
416         mw.widgets.DateInputWidget.prototype.deactivate = function ( userSelected ) {
417                 this.$element.removeClass( 'mw-widget-dateInputWidget-active' );
418                 this.$handle.show();
419                 this.textInput.toggle( false );
420                 this.calendar.toggle( false );
421                 this.setValidityFlag();
423                 if ( userSelected ) {
424                         // Prevent focusing the handle from reopening the calendar
425                         this.closing = true;
426                         this.$handle.trigger( 'focus' );
427                         this.closing = false;
428                 }
430                 this.emit( 'deactivate', !!userSelected );
431         };
433         /**
434          * Activate this input field for data entry. Opens the calendar and shows the text field.
435          *
436          * @private
437          */
438         mw.widgets.DateInputWidget.prototype.activate = function () {
439                 this.calendar.resetUI();
440                 this.$element.addClass( 'mw-widget-dateInputWidget-active' );
441                 this.$handle.hide();
442                 this.textInput.toggle( true );
443                 this.calendar.toggle( true );
445                 this.textInput.$input.trigger( 'focus' );
446         };
448         /**
449          * Get the date format to be used for handle label when the input is inactive.
450          *
451          * @private
452          * @return {string} Format string
453          */
454         mw.widgets.DateInputWidget.prototype.getDisplayFormat = function () {
455                 if ( this.displayFormat !== undefined ) {
456                         return this.displayFormat;
457                 }
459                 if ( this.calendar.getPrecision() === 'month' ) {
460                         return 'MMMM YYYY';
461                 } else {
462                         // The formats Moment.js provides:
463                         // * ll:   Month name, day of month, year
464                         // * lll:  Month name, day of month, year, time
465                         // * llll: Month name, day of month, day of week, year, time
466                         //
467                         // The format we want:
468                         // * ????: Month name, day of month, day of week, year
469                         //
470                         // We try to construct it as 'llll - (lll - ll)' and hope for the best.
471                         // This seems to work well for many languages (maybe even all?).
473                         const localeData = moment.localeData( moment.locale() );
474                         const llll = localeData.longDateFormat( 'llll' );
475                         const lll = localeData.longDateFormat( 'lll' );
476                         const ll = localeData.longDateFormat( 'll' );
477                         let format = llll.replace( lll.replace( ll, '' ), '' );
479                         if ( this.longDisplayFormat ) {
480                                 // Replace MMM to MMMM and ddd to dddd but don't change MMMM and dddd
481                                 format = format.replace( /MMMM?/, 'MMMM' ).replace( /dddd?/, 'dddd' );
482                         }
484                         return format;
485                 }
486         };
488         /**
489          * Get the date format to be used for the text field when the input is active.
490          *
491          * @private
492          * @return {string} Format string
493          */
494         mw.widgets.DateInputWidget.prototype.getInputFormat = function () {
495                 if ( this.inputFormat !== undefined ) {
496                         return this.inputFormat;
497                 }
499                 return {
500                         day: 'YYYY-MM-DD',
501                         month: 'YYYY-MM'
502                 }[ this.calendar.getPrecision() ];
503         };
505         /**
506          * Get the date format to be used internally for the value. This is not configurable in any way,
507          * and always either 'YYYY-MM-DD' or 'YYYY-MM'.
508          *
509          * @private
510          * @return {string} Format string
511          */
512         mw.widgets.DateInputWidget.prototype.getInternalFormat = function () {
513                 return {
514                         day: 'YYYY-MM-DD',
515                         month: 'YYYY-MM'
516                 }[ this.calendar.getPrecision() ];
517         };
519         /**
520          * Get the Moment object for current value.
521          *
522          * @return {Object} Moment object
523          */
524         mw.widgets.DateInputWidget.prototype.getMoment = function () {
525                 return moment( this.getValue(), this.getInternalFormat() );
526         };
528         /**
529          * Handle mouse click events.
530          *
531          * @private
532          * @param {jQuery.Event} e Mouse click event
533          * @return {boolean} False to cancel the default event
534          */
535         mw.widgets.DateInputWidget.prototype.onClick = function ( e ) {
536                 if ( !this.isDisabled() && !this.isReadOnly() && e.which === 1 ) {
537                         this.activate();
538                 }
539                 return false;
540         };
542         /**
543          * Handle key press events.
544          *
545          * @private
546          * @param {jQuery.Event} e Key press event
547          * @return {boolean|undefined} False to cancel the default event
548          */
549         mw.widgets.DateInputWidget.prototype.onKeyPress = function ( e ) {
550                 if ( !this.isDisabled() && !this.isReadOnly() &&
551                         ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
552                 ) {
553                         this.activate();
554                         return false;
555                 }
556         };
558         /**
559          * Handle focus events.
560          *
561          * @private
562          */
563         mw.widgets.DateInputWidget.prototype.onFocus = function () {
564                 if ( !this.isDisabled() && !this.isReadOnly() && !this.closing ) {
565                         this.activate();
566                 }
567         };
569         /**
570          * Handle calendar key press events.
571          *
572          * @private
573          * @param {jQuery.Event} e Key press event
574          * @return {boolean|undefined} False to cancel the default event
575          */
576         mw.widgets.DateInputWidget.prototype.onCalendarKeyPress = function ( e ) {
577                 if ( !this.isDisabled() && e.which === OO.ui.Keys.ENTER ) {
578                         this.deactivate( true );
579                         return false;
580                 }
581         };
583         /**
584          * Handle calendar click events.
585          *
586          * @private
587          * @param {jQuery.Event} e Mouse click event
588          * @return {boolean|undefined} False to cancel the default event
589          */
590         mw.widgets.DateInputWidget.prototype.onCalendarClick = function ( e ) {
591                 const targetClass = this.calendar.getPrecision() === 'month' ?
592                         'mw-widget-calendarWidget-month' :
593                         'mw-widget-calendarWidget-day';
594                 if (
595                         !this.isDisabled() &&
596                         e.which === 1 &&
597                         // eslint-disable-next-line no-jquery/no-class-state
598                         $( e.target ).hasClass( targetClass )
599                 ) {
600                         this.deactivate( true );
601                         return false;
602                 }
603         };
605         /**
606          * Handle text input enter events.
607          *
608          * @private
609          */
610         mw.widgets.DateInputWidget.prototype.onEnter = function () {
611                 this.deactivate( true );
612         };
614         /**
615          * @private
616          * @param {string} date Date string, to be valid, must be in 'YYYY-MM-DD' or 'YYYY-MM' format or
617          *     (unless the field is required) empty
618          * @return {boolean}
619          */
620         mw.widgets.DateInputWidget.prototype.validateDate = function ( date ) {
621                 let isValid;
622                 if ( date === '' ) {
623                         isValid = !this.required;
624                 } else {
625                         isValid = this.isValidDate( date ) && this.isInRange( date );
626                 }
627                 return isValid;
628         };
630         /**
631          * @private
632          * @param {string} date Date string, to be valid, must be in 'YYYY-MM-DD' or 'YYYY-MM' format
633          * @return {boolean}
634          */
635         mw.widgets.DateInputWidget.prototype.isValidDate = function ( date ) {
636                 // "Half-strict mode": for example, for the format 'YYYY-MM-DD', 2015-1-3 instead of 2015-01-03
637                 // is okay, but 2015-01 isn't, and neither is 2015-01-foo. Use Moment's "fuzzy" mode and check
638                 // parsing flags for the details (stolen from implementation of moment#isValid).
639                 const
640                         mom = moment( date, this.getInputFormat() ),
641                         flags = mom.parsingFlags();
643                 return mom.isValid() && flags.charsLeftOver === 0 && flags.unusedTokens.length === 0;
644         };
646         /**
647          * Validates if the date is within the range configured with {@link #cfg-mustBeAfter}
648          * and {@link #cfg-mustBeBefore}.
649          *
650          * @private
651          * @param {string} date Date string, to be valid, must be empty (no date selected) or in
652          *     'YYYY-MM-DD' or 'YYYY-MM' format to be valid
653          * @return {boolean}
654          */
655         mw.widgets.DateInputWidget.prototype.isInRange = function ( date ) {
656                 if ( this.mustBeAfter === undefined && this.mustBeBefore === undefined ) {
657                         return true;
658                 }
659                 const momentDate = moment( date, 'YYYY-MM-DD' );
660                 const isAfter = ( this.mustBeAfter === undefined || momentDate.isAfter( this.mustBeAfter ) );
661                 const isBefore = ( this.mustBeBefore === undefined || momentDate.isBefore( this.mustBeBefore ) );
662                 return isAfter && isBefore;
663         };
665         /**
666          * Get the validity of current value.
667          *
668          * This method returns a promise that resolves if the value is valid and rejects if
669          * it isn't. Uses {@link #validateDate}.
670          *
671          * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
672          */
673         mw.widgets.DateInputWidget.prototype.getValidity = function () {
674                 const isValid = this.validateDate( this.getValue() );
676                 if ( isValid ) {
677                         return $.Deferred().resolve().promise();
678                 } else {
679                         return $.Deferred().reject().promise();
680                 }
681         };
683         /**
684          * Sets the 'invalid' flag appropriately.
685          *
686          * @param {boolean} [isValid] Optionally override validation result
687          */
688         mw.widgets.DateInputWidget.prototype.setValidityFlag = function ( isValid ) {
689                 const setFlag = ( valid ) => {
690                         if ( !valid ) {
691                                 this.$input.attr( 'aria-invalid', 'true' );
692                         } else {
693                                 this.$input.removeAttr( 'aria-invalid' );
694                         }
695                         this.setFlags( { invalid: !valid } );
696                 };
698                 if ( isValid !== undefined ) {
699                         setFlag( isValid );
700                 } else {
701                         this.getValidity().then( () => {
702                                 setFlag( true );
703                         }, () => {
704                                 setFlag( false );
705                         } );
706                 }
707         };
709 }() );