3 // FIXME: mw.htmlform.Element also sets this to empty object
6 function debounce( delay
, callback
) {
9 clearTimeout( timeout
);
10 timeout
= setTimeout( Function
.prototype.apply
.bind( callback
, this, arguments
), delay
);
15 * @class mw.htmlform.Checker
19 * A helper class to add validation to non-OOUI HtmlForm fields.
22 * @param {jQuery} $element Form field generated by HTMLForm
23 * @param {Function} validator Validation callback
24 * @param {string} validator.value Value of the form field to be validated
25 * @param {jQuery.Promise} validator.return The promise should be resolved
26 * with an object with two properties: Boolean 'valid' to indicate success
27 * or failure of validation, and an array 'messages' to be passed to
28 * setErrors() on failure.
30 mw
.htmlform
.Checker = function ( $element
, validator
) {
31 this.validator
= validator
;
32 this.$element
= $element
;
34 this.$errorBox
= $element
.next( '.error' );
35 if ( !this.$errorBox
.length
) {
36 this.$errorBox
= $( '<span>' );
37 this.$errorBox
.hide();
38 $element
.after( this.$errorBox
);
41 this.currentValue
= this.$element
.val();
45 * Attach validation events to the form element
47 * @param {jQuery} [$extraElements] Additional elements to listen for change
49 * @return {mw.htmlform.Checker}
52 mw
.htmlform
.Checker
.prototype.attach = function ( $extraElements
) {
54 // We need to hook to all of these events to be sure we are
55 // notified of all changes to the value of an <input type=text>
57 events
= 'keyup keydown change mouseup cut paste focus blur';
60 if ( $extraElements
) {
61 $e
= $e
.add( $extraElements
);
63 $e
.on( events
, debounce( 1000, this.validate
.bind( this ) ) );
69 * Validate the form element
70 * @return {jQuery.Promise}
72 mw
.htmlform
.Checker
.prototype.validate = function () {
73 var currentRequestInternal
,
75 value
= this.$element
.val();
77 // Abort any pending requests.
78 if ( this.currentRequest
&& this.currentRequest
.abort
) {
79 this.currentRequest
.abort();
83 this.currentValue
= value
;
84 this.setErrors( true, [] );
88 this.currentRequest
= currentRequestInternal
= this.validator( value
)
89 .done( function ( info
) {
90 var forceReplacement
= value
!== that
.currentValue
;
92 // Another request was fired in the meantime, the result we got here is no longer current.
93 // This shouldn't happen as we abort pending requests, but you never know.
94 if ( that
.currentRequest
!== currentRequestInternal
) {
97 // If we're here, then the current request has finished, avoid calling .abort() needlessly.
98 that
.currentRequest
= undefined;
100 that
.currentValue
= value
;
102 that
.setErrors( info
.valid
, info
.messages
, forceReplacement
);
103 } ).fail( function () {
104 that
.currentValue
= null;
105 that
.setErrors( true, [] );
108 return currentRequestInternal
;
112 * Display errors associated with the form element
113 * @param {boolean} valid Whether the input is still valid regardless of the messages
114 * @param {Array} errors Error messages. Each error message will be appended to a
115 * `<span>` or `<li>`, as with jQuery.append().
116 * @param {boolean} [forceReplacement] Set true to force a visual replacement even
117 * if the errors are the same. Ignored if errors are empty.
118 * @return {mw.htmlform.Checker}
121 mw
.htmlform
.Checker
.prototype.setErrors = function ( valid
, errors
, forceReplacement
) {
122 var $oldErrorBox
, tagName
, showFunc
, text
, replace
,
123 $errorBox
= this.$errorBox
;
125 if ( errors
.length
=== 0 ) {
126 // FIXME: Use CSS transition
127 // eslint-disable-next-line no-jquery/no-slide
128 $errorBox
.slideUp( function () {
130 .removeAttr( 'class' )
134 // Match behavior of HTMLFormField::formatErrors(), <span> or <ul>
135 // depending on the count.
136 tagName
= errors
.length
=== 1 ? 'span' : 'ul';
138 // We have to animate the replacement if we're changing the tag. We
139 // also want to if told to by the caller (i.e. to make it visually
140 // obvious that the changed field value gives the same error) or if
141 // the error text changes (because it makes more sense than
142 // changing the text with no animation).
144 forceReplacement
|| $errorBox
.length
> 1 ||
145 $errorBox
[ 0 ].tagName
.toLowerCase() !== tagName
148 text
= $( '<' + tagName
+ '>' )
149 .append( errors
.map( function ( e
) {
150 return errors
.length
=== 1 ? e
: $( '<li>' ).append( e
);
152 if ( text
.text() !== $errorBox
.text() ) {
157 $oldErrorBox
= $errorBox
;
159 this.$errorBox
= $errorBox
= $( '<' + tagName
+ '>' );
161 $oldErrorBox
.after( this.$errorBox
);
164 showFunc = function () {
165 if ( $oldErrorBox
!== $errorBox
) {
167 .removeAttr( 'class' )
170 // FIXME: Use CSS transition
171 // eslint-disable-next-line no-jquery/no-slide
173 .attr( 'class', valid
? 'warning' : 'error' )
175 .append( errors
.map( function ( e
) {
176 return errors
.length
=== 1 ? e
: $( '<li>' ).append( e
);
181 $oldErrorBox
!== $errorBox
&&
182 // eslint-disable-next-line no-jquery/no-class-state
183 ( $oldErrorBox
.hasClass( 'error' ) || $oldErrorBox
.hasClass( 'warning' ) )
185 // eslint-disable-next-line no-jquery/no-slide
186 $oldErrorBox
.slideUp( showFunc
);