Merge "Revert "jquery.textSelection: Remove hardcoded checks for removed WikiEditor...
[lhc/web/wiklou.git] / resources / src / mediawiki / mediawiki.htmlform.js
1 /**
2 * Utility functions for jazzing up HTMLForm elements.
3 *
4 * @class jQuery.plugin.htmlform
5 */
6 ( function ( mw, $ ) {
7
8 var cloneCounter = 0;
9
10 /**
11 * Helper function for hide-if to find the nearby form field.
12 *
13 * Find the closest match for the given name, "closest" being the minimum
14 * level of parents to go to find a form field matching the given name or
15 * ending in array keys matching the given name (e.g. "baz" matches
16 * "foo[bar][baz]").
17 *
18 * @param {jQuery} element
19 * @param {string} name
20 * @return {jQuery|null}
21 */
22 function hideIfGetField( $el, name ) {
23 var sel, $found, $p;
24
25 sel = '[name="' + name + '"],' +
26 '[name="wp' + name + '"],' +
27 '[name$="' + name.replace( /^([^\[]+)/, '[$1]' ) + '"]';
28 for ( $p = $el.parent(); $p.length > 0; $p = $p.parent() ) {
29 $found = $p.find( sel );
30 if ( $found.length > 0 ) {
31 return $found;
32 }
33 }
34 return null;
35 }
36
37 /**
38 * Helper function for hide-if to return a test function and list of
39 * dependent fields for a hide-if specification.
40 *
41 * @param {jQuery} element
42 * @param {Array} hide-if spec
43 * @return {Array} 2 elements: jQuery of dependent fields, and test function
44 */
45 function hideIfParse( $el, spec ) {
46 var op, i, l, v, $field, $fields, func, funcs, getVal;
47
48 op = spec[0];
49 l = spec.length;
50 switch ( op ) {
51 case 'AND':
52 case 'OR':
53 case 'NAND':
54 case 'NOR':
55 funcs = [];
56 $fields = $();
57 for ( i = 1; i < l; i++ ) {
58 if ( !$.isArray( spec[i] ) ) {
59 throw new Error( op + ' parameters must be arrays' );
60 }
61 v = hideIfParse( $el, spec[i] );
62 $fields = $fields.add( v[0] );
63 funcs.push( v[1] );
64 }
65
66 l = funcs.length;
67 switch ( op ) {
68 case 'AND':
69 func = function () {
70 var i;
71 for ( i = 0; i < l; i++ ) {
72 if ( !funcs[i]() ) {
73 return false;
74 }
75 }
76 return true;
77 };
78 break;
79
80 case 'OR':
81 func = function () {
82 var i;
83 for ( i = 0; i < l; i++ ) {
84 if ( funcs[i]() ) {
85 return true;
86 }
87 }
88 return false;
89 };
90 break;
91
92 case 'NAND':
93 func = function () {
94 var i;
95 for ( i = 0; i < l; i++ ) {
96 if ( !funcs[i]() ) {
97 return true;
98 }
99 }
100 return false;
101 };
102 break;
103
104 case 'NOR':
105 func = function () {
106 var i;
107 for ( i = 0; i < l; i++ ) {
108 if ( funcs[i]() ) {
109 return false;
110 }
111 }
112 return true;
113 };
114 break;
115 }
116
117 return [ $fields, func ];
118
119 case 'NOT':
120 if ( l !== 2 ) {
121 throw new Error( 'NOT takes exactly one parameter' );
122 }
123 if ( !$.isArray( spec[1] ) ) {
124 throw new Error( 'NOT parameters must be arrays' );
125 }
126 v = hideIfParse( $el, spec[1] );
127 $fields = v[0];
128 func = v[1];
129 return [ $fields, function () {
130 return !func();
131 } ];
132
133 case '===':
134 case '!==':
135 if ( l !== 3 ) {
136 throw new Error( op + ' takes exactly two parameters' );
137 }
138 $field = hideIfGetField( $el, spec[1] );
139 if ( !$field ) {
140 return [ $(), function () {
141 return false;
142 } ];
143 }
144 v = spec[2];
145
146 if ( $field.first().attr( 'type' ) === 'radio' ||
147 $field.first().attr( 'type' ) === 'checkbox'
148 ) {
149 getVal = function () {
150 var $selected = $field.filter( ':checked' );
151 return $selected.length > 0 ? $selected.val() : '';
152 };
153 } else {
154 getVal = function () {
155 return $field.val();
156 };
157 }
158
159 switch ( op ) {
160 case '===':
161 func = function () {
162 return getVal() === v;
163 };
164 break;
165 case '!==':
166 func = function () {
167 return getVal() !== v;
168 };
169 break;
170 }
171
172 return [ $field, func ];
173
174 default:
175 throw new Error( 'Unrecognized operation \'' + op + '\'' );
176 }
177 }
178
179 /**
180 * jQuery plugin to fade or snap to visible state.
181 *
182 * @param {boolean} [instantToggle=false]
183 * @return {jQuery}
184 * @chainable
185 */
186 $.fn.goIn = function ( instantToggle ) {
187 if ( instantToggle === true ) {
188 return $( this ).show();
189 }
190 return $( this ).stop( true, true ).fadeIn();
191 };
192
193 /**
194 * jQuery plugin to fade or snap to hiding state.
195 *
196 * @param {boolean} [instantToggle=false]
197 * @return jQuery
198 * @chainable
199 */
200 $.fn.goOut = function ( instantToggle ) {
201 if ( instantToggle === true ) {
202 return $( this ).hide();
203 }
204 return $( this ).stop( true, true ).fadeOut();
205 };
206
207 /**
208 * Bind a function to the jQuery object via live(), and also immediately trigger
209 * the function on the objects with an 'instant' parameter set to true.
210 * @param {Function} callback
211 * @param {boolean|jQuery.Event} callback.immediate True when the event is called immediately,
212 * an event object when triggered from an event.
213 */
214 $.fn.liveAndTestAtStart = function ( callback ) {
215 $( this )
216 .live( 'change', callback )
217 .each( function () {
218 callback.call( this, true );
219 } );
220 };
221
222 function enhance( $root ) {
223
224 // Animate the SelectOrOther fields, to only show the text field when
225 // 'other' is selected.
226 $root.find( '.mw-htmlform-select-or-other' ).liveAndTestAtStart( function ( instant ) {
227 var $other = $root.find( '#' + $( this ).attr( 'id' ) + '-other' );
228 $other = $other.add( $other.siblings( 'br' ) );
229 if ( $( this ).val() === 'other' ) {
230 $other.goIn( instant );
231 } else {
232 $other.goOut( instant );
233 }
234 } );
235
236 // Set up hide-if elements
237 $root.find( '.mw-htmlform-hide-if' ).each( function () {
238 var $el = $( this ),
239 spec = $el.data( 'hideIf' ),
240 v, $fields, test, func;
241
242 if ( !spec ) {
243 return;
244 }
245
246 v = hideIfParse( $el, spec );
247 $fields = v[0];
248 test = v[1];
249 func = function () {
250 if ( test() ) {
251 $el.hide();
252 } else {
253 $el.show();
254 }
255 };
256 $fields.change( func );
257 func();
258 } );
259
260 function addMulti( $oldContainer, $container ) {
261 var name = $oldContainer.find( 'input:first-child' ).attr( 'name' ),
262 oldClass = ( ' ' + $oldContainer.attr( 'class' ) + ' ' ).replace( /(mw-htmlform-field-HTMLMultiSelectField|mw-chosen)/g, '' ),
263 $select = $( '<select>' ),
264 dataPlaceholder = mw.message( 'htmlform-chosen-placeholder' );
265 oldClass = $.trim( oldClass );
266 $select.attr( {
267 name: name,
268 multiple: 'multiple',
269 'data-placeholder': dataPlaceholder.plain(),
270 'class': 'htmlform-chzn-select mw-input ' + oldClass
271 } );
272 $oldContainer.find( 'input' ).each( function () {
273 var $oldInput = $( this ),
274 checked = $oldInput.prop( 'checked' ),
275 $option = $( '<option>' );
276 $option.prop( 'value', $oldInput.prop( 'value' ) );
277 if ( checked ) {
278 $option.prop( 'selected', true );
279 }
280 $option.text( $oldInput.prop( 'value' ) );
281 $select.append( $option );
282 } );
283 $container.append( $select );
284 }
285
286 function convertCheckboxesToMulti( $oldContainer, type ) {
287 var $fieldLabel = $( '<td>' ),
288 $td = $( '<td>' ),
289 $fieldLabelText = $( '<label>' ),
290 $container;
291 if ( type === 'tr' ) {
292 addMulti( $oldContainer, $td );
293 $container = $( '<tr>' );
294 $container.append( $td );
295 } else if ( type === 'div' ) {
296 $fieldLabel = $( '<div>' );
297 $container = $( '<div>' );
298 addMulti( $oldContainer, $container );
299 }
300 $fieldLabel.attr( 'class', 'mw-label' );
301 $fieldLabelText.text( $oldContainer.find( '.mw-label label' ).text() );
302 $fieldLabel.append( $fieldLabelText );
303 $container.prepend( $fieldLabel );
304 $oldContainer.replaceWith( $container );
305 return $container;
306 }
307
308 if ( $root.find( '.mw-chosen' ).length ) {
309 mw.loader.using( 'jquery.chosen', function () {
310 $root.find( '.mw-chosen' ).each( function () {
311 var type = this.nodeName.toLowerCase(),
312 $converted = convertCheckboxesToMulti( $( this ), type );
313 $converted.find( '.htmlform-chzn-select' ).chosen( { width: 'auto' } );
314 } );
315 } );
316 }
317
318 var $matrixTooltips = $root.find( '.mw-htmlform-matrix .mw-htmlform-tooltip' );
319 if ( $matrixTooltips.length ) {
320 mw.loader.using( 'jquery.tipsy', function () {
321 $matrixTooltips.tipsy( { gravity: 's' } );
322 } );
323 }
324
325 // Add/remove cloner clones without having to resubmit the form
326 $root.find( '.mw-htmlform-cloner-delete-button' ).click( function ( ev ) {
327 ev.preventDefault();
328 $( this ).closest( 'li.mw-htmlform-cloner-li' ).remove();
329 } );
330
331 $root.find( '.mw-htmlform-cloner-create-button' ).click( function ( ev ) {
332 var $ul, $li, html;
333
334 ev.preventDefault();
335
336 $ul = $( this ).prev( 'ul.mw-htmlform-cloner-ul' );
337
338 html = $ul.data( 'template' ).replace(
339 $ul.data( 'uniqueId' ), 'clone' + ( ++cloneCounter ), 'g'
340 );
341
342 $li = $( '<li>' )
343 .addClass( 'mw-htmlform-cloner-li' )
344 .html( html )
345 .appendTo( $ul );
346
347 enhance( $li );
348 } );
349
350 mw.hook( 'htmlform.enhance' ).fire( $root );
351
352 }
353
354 $( function () {
355 enhance( $( document ) );
356 } );
357
358 /**
359 * @class jQuery
360 * @mixins jQuery.plugin.htmlform
361 */
362 }( mediaWiki, jQuery ) );