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