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