summaryrefslogtreecommitdiff
path: root/www/wiki/resources/src/mediawiki/htmlform/htmlform.Checker.js
blob: 3f53b63600455dd4b2a454bf7a839bceeb849901 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
( 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 = $( '<span>' );
			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 <input type=text>
			// 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
	 *  `<span>` or `<li>`, 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(), <span> or <ul>
			// 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 : $( '<li>' ).append( e );
					} ) );
				if ( text.text() !== $errorBox.text() ) {
					replace = true;
				}
			}

			$oldErrorBox = $errorBox;
			if ( replace ) {
				this.$errorBox = $errorBox = $( '<' + tagName + '>' );
				$errorBox.hide();
				$oldErrorBox.after( this.$errorBox );
			}

			showFunc = function () {
				if ( $oldErrorBox !== $errorBox ) {
					$oldErrorBox
						.removeAttr( 'class' )
						.detach();
				}
				$errorBox
					.attr( 'class', 'error' )
					.empty()
					.append( errors.map( function ( e ) {
						return errors.length === 1 ? e : $( '<li>' ).append( e );
					} ) )
					.slideDown();
			};
			if ( $oldErrorBox !== $errorBox && $oldErrorBox.hasClass( 'error' ) ) {
				$oldErrorBox.slideUp( showFunc );
			} else {
				showFunc();
			}
		}

		return this;
	};

}( mediaWiki, jQuery ) );