( function ( mw, $ ) {
mw.htmlform = {};
/**
* @class mw.htmlform.Checker
*/
/**
* A helper class to add validation to non-OOUI HtmlForm fields.
*
* @constructor
* @param {jQuery} $element Form field generated by HTMLForm
* @param {Function} validator Validation callback
* @param {string} validator.value Value of the form field to be validated
* @param {jQuery.Promise} validator.return The promise should be resolved
* with an object with two properties: Boolean 'valid' to indicate success
* or failure of validation, and an array 'messages' to be passed to
* setErrors() on failure.
*/
mw.htmlform.Checker = function ( $element, validator ) {
this.validator = validator;
this.$element = $element;
this.$errorBox = $element.next( '.error' );
if ( !this.$errorBox.length ) {
this.$errorBox = $( '' );
this.$errorBox.hide();
$element.after( this.$errorBox );
}
this.currentValue = this.$element.val();
};
/**
* Attach validation events to the form element
*
* @param {jQuery} [$extraElements] Additional elements to listen for change
* events on.
* @return {mw.htmlform.Checker}
* @chainable
*/
mw.htmlform.Checker.prototype.attach = function ( $extraElements ) {
var $e,
// We need to hook to all of these events to be sure we are
// notified of all changes to the value of an
// field.
events = 'keyup keydown change mouseup cut paste focus blur';
$e = this.$element;
if ( $extraElements ) {
$e = $e.add( $extraElements );
}
$e.on( events, $.debounce( 1000, this.validate.bind( this ) ) );
return this;
};
/**
* Validate the form element
* @return {jQuery.Promise}
*/
mw.htmlform.Checker.prototype.validate = function () {
var currentRequestInternal,
that = this,
value = this.$element.val();
// Abort any pending requests.
if ( this.currentRequest && this.currentRequest.abort ) {
this.currentRequest.abort();
}
if ( value === '' ) {
this.currentValue = value;
this.setErrors( [] );
return;
}
this.currentRequest = currentRequestInternal = this.validator( value )
.done( function ( info ) {
var forceReplacement = value !== that.currentValue;
// Another request was fired in the meantime, the result we got here is no longer current.
// This shouldn't happen as we abort pending requests, but you never know.
if ( that.currentRequest !== currentRequestInternal ) {
return;
}
// If we're here, then the current request has finished, avoid calling .abort() needlessly.
that.currentRequest = undefined;
that.currentValue = value;
if ( info.valid ) {
that.setErrors( [], forceReplacement );
} else {
that.setErrors( info.messages, forceReplacement );
}
} ).fail( function () {
that.currentValue = null;
that.setErrors( [] );
} );
return currentRequestInternal;
};
/**
* Display errors associated with the form element
* @param {Array} errors Error messages. Each error message will be appended to a
* `` or `
`, as with jQuery.append().
* @param {boolean} [forceReplacement] Set true to force a visual replacement even
* if the errors are the same. Ignored if errors are empty.
* @return {mw.htmlform.Checker}
* @chainable
*/
mw.htmlform.Checker.prototype.setErrors = function ( errors, forceReplacement ) {
var $oldErrorBox, tagName, showFunc, text, replace,
$errorBox = this.$errorBox;
if ( errors.length === 0 ) {
$errorBox.slideUp( function () {
$errorBox
.removeAttr( 'class' )
.empty();
} );
} else {
// Match behavior of HTMLFormField::formatErrors(), or
// depending on the count.
tagName = errors.length === 1 ? 'span' : 'ul';
// We have to animate the replacement if we're changing the tag. We
// also want to if told to by the caller (i.e. to make it visually
// obvious that the changed field value gives the same error) or if
// the error text changes (because it makes more sense than
// changing the text with no animation).
replace = (
forceReplacement || $errorBox.length > 1 ||
$errorBox[ 0 ].tagName.toLowerCase() !== tagName
);
if ( !replace ) {
text = $( '<' + tagName + '>' )
.append( errors.map( function ( e ) {
return errors.length === 1 ? e : $( '