Merge "rdbms: make transaction rounds apply DBO_TRX to DB_REPLICA connections"
[mediawiki.git] / resources / src / mediawiki.special.createaccount / HtmlformChecker.js
blob1a542d3cb17c69ed9276098a0b1aff737354ff6a
1 const util = require( 'mediawiki.util' );
3 /**
4  * A helper class to add validation to non-OOUI HTMLForm fields.
5  *
6  * @private
7  * @class HtmlformChecker
8  *
9  * @constructor
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.
18  */
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 );
28         }
30         this.currentValue = this.$element.val();
33 /**
34  * Attach validation events to the form element
35  *
36  * @param {jQuery} [$extraElements] Additional elements to listen for change
37  *  events on.
38  * @return {HtmlformChecker}
39  * @chainable
40  */
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>
45         // field.
46         const events = 'keyup keydown change mouseup cut paste focus blur';
48         if ( $extraElements ) {
49                 $e = $e.add( $extraElements );
50         }
51         $e.on( events, mw.util.debounce( this.validate.bind( this ), 1000 ) );
53         return this;
56 /**
57  * Validate the form element
58  */
59 HtmlformChecker.prototype.validate = function () {
60         const value = this.$element.val();
62         // Abort any pending requests.
63         if ( this.abortController ) {
64                 this.abortController.abort();
65         }
66         this.abortController = new mw.Api.AbortController();
68         if ( value === '' ) {
69                 this.currentValue = value;
70                 this.setErrors( true, [] );
71                 return;
72         }
74         this.validator( value, this.abortController.signal )
75                 .then( ( info ) => {
76                         const forceReplacement = value !== this.currentValue;
78                         this.currentValue = value;
80                         this.setErrors( info.valid, info.messages, forceReplacement );
81                 } ).catch( () => {
82                         this.currentValue = null;
83                         this.setErrors( true, [] );
84                 } );
87 /**
88  * Display errors associated with the form element
89  *
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}
97  * @chainable
98  */
99 HtmlformChecker.prototype.setErrors = function ( valid, errors, forceReplacement ) {
100         let replace;
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( () => {
107                         $errorBox
108                                 .removeAttr( 'class' )
109                                 .empty();
110                 } );
111         } else {
112                 let $error;
113                 // Match behavior of HTMLFormField::formatErrors()
114                 if ( errors.length === 1 ) {
115                         $error = $( '<div>' ).append( errors[ 0 ] );
116                 } else {
117                         $error = $( '<ul>' ).append(
118                                 errors.map( ( e ) => $( '<li>' ).append( e ) )
119                         );
120                 }
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() ) {
128                         replace = true;
129                 }
131                 const $oldErrorBox = $errorBox;
132                 if ( replace ) {
133                         this.$errorBox = $errorBox = $( '<div>' );
134                         $errorBox.hide();
135                         $oldErrorBox.after( this.$errorBox );
136                 }
138                 const oldErrorType = this.oldErrorType || 'notice';
139                 const errorType = valid ? 'warning' : 'error';
140                 this.oldErrorType = errorType;
141                 const showFunc = function () {
142                         if ( $oldErrorBox !== $errorBox ) {
143                                 $oldErrorBox
144                                         .removeAttr( 'class' )
145                                         .detach();
146                         }
147                         $errorBox.empty();
148                         $errorBox.append(
149                                 util.messageBox( $error[ 0 ], errorType )
150                         );
151                         // FIXME: Use CSS transition
152                         // eslint-disable-next-line no-jquery/no-slide
153                         $errorBox.slideDown();
154                 };
155                 if (
156                         $oldErrorBox !== $errorBox &&
157                         ( oldErrorType === 'error' || oldErrorType === 'warning' )
158                 ) {
159                         // eslint-disable-next-line no-jquery/no-slide
160                         $oldErrorBox.slideUp( showFunc );
161                 } else {
162                         showFunc();
163                 }
164         }
166         return this;
169 module.exports = HtmlformChecker;