1 const util = require( 'mediawiki.util' );
4 * A helper class to add validation to non-OOUI HTMLForm fields.
7 * @class HtmlformChecker
10 * @param {jQuery} $element Form field generated by HTMLForm
11 * @param {Function} validator Validation callback
12 * @param {string} validator.value Value of the form field to be validated
13 * @param {AbortSignal} validator.signal Used to signal if the response is no longer needed
14 * @param {jQuery.Promise} validator.return The promise should be resolved
15 * with an object with two properties: Boolean 'valid' to indicate success
16 * or failure of validation, and an array (containing HTML strings or
17 * jQuery collections) 'messages' to be passed to setErrors() on failure.
19 function HtmlformChecker( $element, validator ) {
20 this.validator = validator;
21 this.$element = $element;
23 this.$errorBox = $element.next( '.html-form-error' );
24 if ( !this.$errorBox.length ) {
25 this.$errorBox = $( '<div>' ).addClass( 'html-form-error' );
26 this.$errorBox.hide();
27 $element.after( this.$errorBox );
30 this.currentValue = this.$element.val();
34 * Attach validation events to the form element
36 * @param {jQuery} [$extraElements] Additional elements to listen for change
38 * @return {HtmlformChecker}
41 HtmlformChecker.prototype.attach = function ( $extraElements ) {
42 let $e = this.$element;
43 // We need to hook to all of these events to be sure we are
44 // notified of all changes to the value of an <input type=text>
46 const events = 'keyup keydown change mouseup cut paste focus blur';
48 if ( $extraElements ) {
49 $e = $e.add( $extraElements );
51 $e.on( events, mw.util.debounce( this.validate.bind( this ), 1000 ) );
57 * Validate the form element
59 HtmlformChecker.prototype.validate = function () {
60 const value = this.$element.val();
62 // Abort any pending requests.
63 if ( this.abortController ) {
64 this.abortController.abort();
66 this.abortController = new mw.Api.AbortController();
69 this.currentValue = value;
70 this.setErrors( true, [] );
74 this.validator( value, this.abortController.signal )
76 const forceReplacement = value !== this.currentValue;
78 this.currentValue = value;
80 this.setErrors( info.valid, info.messages, forceReplacement );
82 this.currentValue = null;
83 this.setErrors( true, [] );
88 * Display errors associated with the form element
90 * @param {boolean} valid Whether the input is still valid regardless of the messages
91 * @param {Array} errors A list of error messages with formatting. Each message may be
92 * a string (which will be interpreted as HTML) or a jQuery collection. They will
93 * be appended to `<div>` or `<li>`, as with jQuery.append().
94 * @param {boolean} [forceReplacement] Set true to force a visual replacement even
95 * if the errors are the same. Ignored if errors are empty.
96 * @return {HtmlformChecker}
99 HtmlformChecker.prototype.setErrors = function ( valid, errors, forceReplacement ) {
101 let $errorBox = this.$errorBox;
103 if ( errors.length === 0 ) {
104 // FIXME: Use CSS transition
105 // eslint-disable-next-line no-jquery/no-slide
106 $errorBox.slideUp( () => {
108 .removeAttr( 'class' )
113 // Match behavior of HTMLFormField::formatErrors()
114 if ( errors.length === 1 ) {
115 $error = $( '<div>' ).append( errors[ 0 ] );
117 $error = $( '<ul>' ).append(
118 errors.map( ( e ) => $( '<li>' ).append( e ) )
122 // Animate the replacement if told to by the caller (i.e. to make it visually
123 // obvious that the changed field value gives the same errorbox) or if
124 // the errorbox text changes (because it makes more sense than
125 // changing the text with no animation).
126 replace = forceReplacement;
127 if ( !replace && $error.text() !== $errorBox.text() ) {
131 const $oldErrorBox = $errorBox;
133 this.$errorBox = $errorBox = $( '<div>' );
135 $oldErrorBox.after( this.$errorBox );
138 const oldErrorType = this.oldErrorType || 'notice';
139 const errorType = valid ? 'warning' : 'error';
140 this.oldErrorType = errorType;
141 const showFunc = function () {
142 if ( $oldErrorBox !== $errorBox ) {
144 .removeAttr( 'class' )
149 util.messageBox( $error[ 0 ], errorType )
151 // FIXME: Use CSS transition
152 // eslint-disable-next-line no-jquery/no-slide
153 $errorBox.slideDown();
156 $oldErrorBox !== $errorBox &&
157 ( oldErrorType === 'error' || oldErrorType === 'warning' )
159 // eslint-disable-next-line no-jquery/no-slide
160 $oldErrorBox.slideUp( showFunc );
169 module.exports = HtmlformChecker;