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