Update account creation form validation
[lhc/web/wiklou.git] / resources / src / mediawiki / htmlform / htmlform.Checker.js
1 ( function ( mw, $ ) {
2
3 mw.htmlform = {};
4
5 /**
6 * @class mw.htmlform.Checker
7 */
8
9 /**
10 * A helper class to add validation to non-OOUI HtmlForm fields.
11 *
12 * @constructor
13 * @param {jQuery} $element Form field generated by HTMLForm
14 * @param {Function} validator Validation callback
15 * @param {string} validator.value Value of the form field to be validated
16 * @param {jQuery.Promise} validator.return The promise should be resolved
17 * with an object with two properties: Boolean 'valid' to indicate success
18 * or failure of validation, and an array 'messages' to be passed to
19 * setErrors() on failure.
20 */
21 mw.htmlform.Checker = function ( $element, validator ) {
22 this.validator = validator;
23 this.$element = $element;
24
25 this.$errorBox = $element.next( '.error' );
26 if ( !this.$errorBox.length ) {
27 this.$errorBox = $( '<span>' );
28 this.$errorBox.hide();
29 $element.after( this.$errorBox );
30 }
31
32 this.currentValue = this.$element.val();
33 };
34
35 /**
36 * Attach validation events to the form element
37 *
38 * @param {jQuery} [$extraElements] Additional elements to listen for change
39 * events on.
40 * @return {mw.htmlform.Checker}
41 * @chainable
42 */
43 mw.htmlform.Checker.prototype.attach = function ( $extraElements ) {
44 var $e,
45 // We need to hook to all of these events to be sure we are
46 // notified of all changes to the value of an <input type=text>
47 // field.
48 events = 'keyup keydown change mouseup cut paste focus blur';
49
50 $e = this.$element;
51 if ( $extraElements ) {
52 $e = $e.add( $extraElements );
53 }
54 $e.on( events, $.debounce( 1000, this.validate.bind( this ) ) );
55
56 return this;
57 };
58
59 /**
60 * Validate the form element
61 * @return {jQuery.Promise}
62 */
63 mw.htmlform.Checker.prototype.validate = function () {
64 var currentRequestInternal,
65 that = this,
66 value = this.$element.val();
67
68 // Abort any pending requests.
69 if ( this.currentRequest && this.currentRequest.abort ) {
70 this.currentRequest.abort();
71 }
72
73 if ( value === '' ) {
74 this.currentValue = value;
75 this.setErrors( [] );
76 return;
77 }
78
79 this.currentRequest = currentRequestInternal = this.validator( value )
80 .done( function ( info ) {
81 var forceReplacement = value !== that.currentValue;
82
83 // Another request was fired in the meantime, the result we got here is no longer current.
84 // This shouldn't happen as we abort pending requests, but you never know.
85 if ( that.currentRequest !== currentRequestInternal ) {
86 return;
87 }
88 // If we're here, then the current request has finished, avoid calling .abort() needlessly.
89 that.currentRequest = undefined;
90
91 that.currentValue = value;
92
93 if ( info.valid ) {
94 that.setErrors( [], forceReplacement );
95 } else {
96 that.setErrors( info.messages, forceReplacement );
97 }
98 } ).fail( function () {
99 that.currentValue = null;
100 that.setErrors( [] );
101 } );
102
103 return currentRequestInternal;
104 };
105
106 /**
107 * Display errors associated with the form element
108 * @param {Array} errors Error messages. Each error message will be appended to a
109 * `<span>` or `<li>`, as with jQuery.append().
110 * @param {boolean} [forceReplacement] Set true to force a visual replacement even
111 * if the errors are the same. Ignored if errors are empty.
112 * @return {mw.htmlform.Checker}
113 * @chainable
114 */
115 mw.htmlform.Checker.prototype.setErrors = function ( errors, forceReplacement ) {
116 var $oldErrorBox, tagName, showFunc, text, replace,
117 $errorBox = this.$errorBox;
118
119 if ( errors.length === 0 ) {
120 $errorBox.slideUp( function () {
121 $errorBox
122 .removeAttr( 'class' )
123 .empty();
124 } );
125 } else {
126 // Match behavior of HTMLFormField::formatErrors(), <span> or <ul>
127 // depending on the count.
128 tagName = errors.length === 1 ? 'span' : 'ul';
129
130 // We have to animate the replacement if we're changing the tag. We
131 // also want to if told to by the caller (i.e. to make it visually
132 // obvious that the changed field value gives the same error) or if
133 // the error text changes (because it makes more sense than
134 // changing the text with no animation).
135 replace = (
136 forceReplacement || $errorBox.length > 1 ||
137 $errorBox[ 0 ].tagName.toLowerCase() !== tagName
138 );
139 if ( !replace ) {
140 text = $( '<' + tagName + '>' )
141 .append( errors.map( function ( e ) {
142 return errors.length === 1 ? e : $( '<li>' ).append( e );
143 } ) );
144 if ( text.text() !== $errorBox.text() ) {
145 replace = true;
146 }
147 }
148
149 $oldErrorBox = $errorBox;
150 if ( replace ) {
151 this.$errorBox = $errorBox = $( '<' + tagName + '>' );
152 $errorBox.hide();
153 $oldErrorBox.after( this.$errorBox );
154 }
155
156 showFunc = function () {
157 if ( $oldErrorBox !== $errorBox ) {
158 $oldErrorBox
159 .removeAttr( 'class' )
160 .detach();
161 }
162 $errorBox
163 .attr( 'class', 'error' )
164 .empty()
165 .append( errors.map( function ( e ) {
166 return errors.length === 1 ? e : $( '<li>' ).append( e );
167 } ) )
168 .slideDown();
169 };
170 if ( $oldErrorBox !== $errorBox && $oldErrorBox.hasClass( 'error' ) ) {
171 $oldErrorBox.slideUp( showFunc );
172 } else {
173 showFunc();
174 }
175 }
176
177 return this;
178 };
179
180 }( mediaWiki, jQuery ) );