]> git.openstreetmap.org Git - osqa.git/blob - forum/skins/default/media/js/jquery.form.js
Better search, mixing FTS and exact match and better related questions functionality.
[osqa.git] / forum / skins / default / media / js / jquery.form.js
1 /*
2  * jQuery Form Plugin
3  * version: 2.33 (22-SEP-2009)
4  * @requires jQuery v1.2.6 or later
5  *
6  * Examples and documentation at: http://malsup.com/jquery/form/
7  * Dual licensed under the MIT and GPL licenses:
8  *   http://www.opensource.org/licenses/mit-license.php
9  *   http://www.gnu.org/licenses/gpl.html
10  */
11 ;(function($) {
12
13 /*
14         Usage Note:
15         -----------
16         Do not use both ajaxSubmit and ajaxForm on the same form.  These
17         functions are intended to be exclusive.  Use ajaxSubmit if you want
18         to bind your own submit handler to the form.  For example,
19
20         $(document).ready(function() {
21                 $('#myForm').bind('submit', function() {
22                         $(this).ajaxSubmit({
23                                 target: '#output'
24                         });
25                         return false; // <-- important!
26                 });
27         });
28
29         Use ajaxForm when you want the plugin to manage all the event binding
30         for you.  For example,
31
32         $(document).ready(function() {
33                 $('#myForm').ajaxForm({
34                         target: '#output'
35                 });
36         });
37
38         When using ajaxForm, the ajaxSubmit function will be invoked for you
39         at the appropriate time.
40 */
41
42 /**
43  * ajaxSubmit() provides a mechanism for immediately submitting
44  * an HTML form using AJAX.
45  */
46 $.fn.ajaxSubmit = function(options) {
47         // fast fail if nothing selected (http://dev.jquery.com/ticket/2752)
48         if (!this.length) {
49                 log('ajaxSubmit: skipping submit process - no element selected');
50                 return this;
51         }
52
53         if (typeof options == 'function')
54                 options = { success: options };
55
56         var url = $.trim(this.attr('action'));
57         if (url) {
58                 // clean url (don't include hash vaue)
59                 url = (url.match(/^([^#]+)/)||[])[1];
60         }
61         url = url || window.location.href || '';
62
63         options = $.extend({
64                 url:  url,
65                 type: this.attr('method') || 'GET'
66         }, options || {});
67
68         // hook for manipulating the form data before it is extracted;
69         // convenient for use with rich editors like tinyMCE or FCKEditor
70         var veto = {};
71         this.trigger('form-pre-serialize', [this, options, veto]);
72         if (veto.veto) {
73                 log('ajaxSubmit: submit vetoed via form-pre-serialize trigger');
74                 return this;
75         }
76
77         // provide opportunity to alter form data before it is serialized
78         if (options.beforeSerialize && options.beforeSerialize(this, options) === false) {
79                 log('ajaxSubmit: submit aborted via beforeSerialize callback');
80                 return this;
81         }
82
83         var a = this.formToArray(options.semantic);
84         if (options.data) {
85                 options.extraData = options.data;
86                 for (var n in options.data) {
87                   if(options.data[n] instanceof Array) {
88                         for (var k in options.data[n])
89                           a.push( { name: n, value: options.data[n][k] } );
90                   }
91                   else
92                          a.push( { name: n, value: options.data[n] } );
93                 }
94         }
95
96         // give pre-submit callback an opportunity to abort the submit
97         if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) {
98                 log('ajaxSubmit: submit aborted via beforeSubmit callback');
99                 return this;
100         }
101
102         // fire vetoable 'validate' event
103         this.trigger('form-submit-validate', [a, this, options, veto]);
104         if (veto.veto) {
105                 log('ajaxSubmit: submit vetoed via form-submit-validate trigger');
106                 return this;
107         }
108
109         var q = $.param(a);
110
111         if (options.type.toUpperCase() == 'GET') {
112                 options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q;
113                 options.data = null;  // data is null for 'get'
114         }
115         else
116                 options.data = q; // data is the query string for 'post'
117
118         var $form = this, callbacks = [];
119         if (options.resetForm) callbacks.push(function() { $form.resetForm(); });
120         if (options.clearForm) callbacks.push(function() { $form.clearForm(); });
121
122         // perform a load on the target only if dataType is not provided
123         if (!options.dataType && options.target) {
124                 var oldSuccess = options.success || function(){};
125                 callbacks.push(function(data) {
126                         $(options.target).html(data).each(oldSuccess, arguments);
127                 });
128         }
129         else if (options.success)
130                 callbacks.push(options.success);
131
132         options.success = function(data, status) {
133                 for (var i=0, max=callbacks.length; i < max; i++)
134                         callbacks[i].apply(options, [data, status, $form]);
135         };
136
137         // are there files to upload?
138         var files = $('input:file', this).fieldValue();
139         var found = false;
140         for (var j=0; j < files.length; j++)
141                 if (files[j])
142                         found = true;
143
144         var multipart = false;
145 //      var mp = 'multipart/form-data';
146 //      multipart = ($form.attr('enctype') == mp || $form.attr('encoding') == mp);
147
148         // options.iframe allows user to force iframe mode
149    if (options.iframe || found || multipart) {
150            // hack to fix Safari hang (thanks to Tim Molendijk for this)
151            // see:  http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d
152            if (options.closeKeepAlive)
153                    $.get(options.closeKeepAlive, fileUpload);
154            else
155                    fileUpload();
156            }
157    else{
158            $.ajax(options);
159    }
160
161         // fire 'notify' event
162         this.trigger('form-submit-notify', [this, options]);
163         return this;
164
165
166         // private function for handling file uploads (hat tip to YAHOO!)
167         function fileUpload() {
168                 var form = $form[0];
169
170                 if ($(':input[name=submit]', form).length) {
171                         alert('Error: Form elements must not be named "submit".');
172                         return;
173                 }
174
175                 var opts = $.extend({}, $.ajaxSettings, options);
176                 var s = $.extend(true, {}, $.extend(true, {}, $.ajaxSettings), opts);
177
178                 var id = 'jqFormIO' + (new Date().getTime());
179                 var $io = $('<iframe id="' + id + '" name="' + id + '" src="about:blank" />');
180                 var io = $io[0];
181
182                 $io.css({ position: 'absolute', top: '-1000px', left: '-1000px' });
183
184                 var xhr = { // mock object
185                         aborted: 0,
186                         responseText: null,
187                         responseXML: null,
188                         status: 0,
189                         statusText: 'n/a',
190                         getAllResponseHeaders: function() {},
191                         getResponseHeader: function() {},
192                         setRequestHeader: function() {},
193                         abort: function() {
194                                 this.aborted = 1;
195                                 $io.attr('src','about:blank'); // abort op in progress
196                         }
197                 };
198
199                 var g = opts.global;
200                 // trigger ajax global events so that activity/block indicators work like normal
201                 if (g && ! $.active++) $.event.trigger("ajaxStart");
202                 if (g) $.event.trigger("ajaxSend", [xhr, opts]);
203
204                 if (s.beforeSend && s.beforeSend(xhr, s) === false) {
205                         s.global && $.active--;
206                         return;
207                 }
208                 if (xhr.aborted)
209                         return;
210
211                 var cbInvoked = 0;
212                 var timedOut = 0;
213
214                 // add submitting element to data if we know it
215                 var sub = form.clk;
216                 if (sub) {
217                         var n = sub.name;
218                         if (n && !sub.disabled) {
219                                 options.extraData = options.extraData || {};
220                                 options.extraData[n] = sub.value;
221                                 if (sub.type == "image") {
222                                         options.extraData[name+'.x'] = form.clk_x;
223                                         options.extraData[name+'.y'] = form.clk_y;
224                                 }
225                         }
226                 }
227
228                 // take a breath so that pending repaints get some cpu time before the upload starts
229                 setTimeout(function() {
230                         // make sure form attrs are set
231                         var t = $form.attr('target'), a = $form.attr('action');
232
233                         // update form attrs in IE friendly way
234                         form.setAttribute('target',id);
235                         if (form.getAttribute('method') != 'POST')
236                                 form.setAttribute('method', 'POST');
237                         if (form.getAttribute('action') != opts.url)
238                                 form.setAttribute('action', opts.url);
239
240                         // ie borks in some cases when setting encoding
241                         if (! options.skipEncodingOverride) {
242                                 $form.attr({
243                                         encoding: 'multipart/form-data',
244                                         enctype:  'multipart/form-data'
245                                 });
246                         }
247
248                         // support timout
249                         if (opts.timeout)
250                                 setTimeout(function() { timedOut = true; cb(); }, opts.timeout);
251
252                         // add "extra" data to form if provided in options
253                         var extraInputs = [];
254                         try {
255                                 if (options.extraData)
256                                         for (var n in options.extraData)
257                                                 extraInputs.push(
258                                                         $('<input type="hidden" name="'+n+'" value="'+options.extraData[n]+'" />')
259                                                                 .appendTo(form)[0]);
260
261                                 // add iframe to doc and submit the form
262                                 $io.appendTo('body');
263                                 io.attachEvent ? io.attachEvent('onload', cb) : io.addEventListener('load', cb, false);
264                                 form.submit();
265                         }
266                         finally {
267                                 // reset attrs and remove "extra" input elements
268                                 form.setAttribute('action',a);
269                                 t ? form.setAttribute('target', t) : $form.removeAttr('target');
270                                 $(extraInputs).remove();
271                         }
272                 }, 10);
273
274                 var domCheckCount = 50;
275
276                 function cb() {
277                         if (cbInvoked++) return;
278
279                         io.detachEvent ? io.detachEvent('onload', cb) : io.removeEventListener('load', cb, false);
280
281                         var ok = true;
282                         try {
283                                 if (timedOut) throw 'timeout';
284                                 // extract the server response from the iframe
285                                 var data, doc;
286
287                                 doc = io.contentWindow ? io.contentWindow.document : io.contentDocument ? io.contentDocument : io.document;
288                                 
289                                 var isXml = opts.dataType == 'xml' || doc.XMLDocument || $.isXMLDoc(doc);
290                                 log('isXml='+isXml);
291                                 if (!isXml && (doc.body == null || doc.body.innerHTML == '')) {
292                                         if (--domCheckCount) {
293                                                 // in some browsers (Opera) the iframe DOM is not always traversable when
294                                                 // the onload callback fires, so we loop a bit to accommodate
295                                                 cbInvoked = 0;
296                                                 setTimeout(cb, 100);
297                                                 return;
298                                         }
299                                         log('Could not access iframe DOM after 50 tries.');
300                                         return;
301                                 }
302
303                                 xhr.responseText = doc.body ? doc.body.innerHTML : null;
304                                 xhr.responseXML = doc.XMLDocument ? doc.XMLDocument : doc;
305                                 xhr.getResponseHeader = function(header){
306                                         var headers = {'content-type': opts.dataType};
307                                         return headers[header];
308                                 };
309
310                                 if (opts.dataType == 'json' || opts.dataType == 'script') {
311                                         // see if user embedded response in textarea
312                                         var ta = doc.getElementsByTagName('textarea')[0];
313                                         if (ta)
314                                                 xhr.responseText = ta.value;
315                                         else {
316                                                 // account for browsers injecting pre around json response
317                                                 var pre = doc.getElementsByTagName('pre')[0];
318                                                 if (pre)
319                                                         xhr.responseText = pre.innerHTML;
320                                         }                         
321                                 }
322                                 else if (opts.dataType == 'xml' && !xhr.responseXML && xhr.responseText != null) {
323                                         xhr.responseXML = toXml(xhr.responseText);
324                                 }
325                                 data = $.httpData(xhr, opts.dataType);
326                         }
327                         catch(e){
328                                 ok = false;
329                                 $.handleError(opts, xhr, 'error', e);
330                         }
331
332                         // ordering of these callbacks/triggers is odd, but that's how $.ajax does it
333                         if (ok) {
334                                 opts.success(data, 'success');
335                                 if (g) $.event.trigger("ajaxSuccess", [xhr, opts]);
336                         }
337                         if (g) $.event.trigger("ajaxComplete", [xhr, opts]);
338                         if (g && ! --$.active) $.event.trigger("ajaxStop");
339                         if (opts.complete) opts.complete(xhr, ok ? 'success' : 'error');
340
341                         // clean up
342                         setTimeout(function() {
343                                 $io.remove();
344                                 xhr.responseXML = null;
345                         }, 100);
346                 };
347
348                 function toXml(s, doc) {
349                         if (window.ActiveXObject) {
350                                 doc = new ActiveXObject('Microsoft.XMLDOM');
351                                 doc.async = 'false';
352                                 doc.loadXML(s);
353                         }
354                         else
355                                 doc = (new DOMParser()).parseFromString(s, 'text/xml');
356                         return (doc && doc.documentElement && doc.documentElement.tagName != 'parsererror') ? doc : null;
357                 };
358         };
359 };
360
361 /**
362  * ajaxForm() provides a mechanism for fully automating form submission.
363  *
364  * The advantages of using this method instead of ajaxSubmit() are:
365  *
366  * 1: This method will include coordinates for <input type="image" /> elements (if the element
367  *      is used to submit the form).
368  * 2. This method will include the submit element's name/value data (for the element that was
369  *      used to submit the form).
370  * 3. This method binds the submit() method to the form for you.
371  *
372  * The options argument for ajaxForm works exactly as it does for ajaxSubmit.  ajaxForm merely
373  * passes the options argument along after properly binding events for submit elements and
374  * the form itself.
375  */
376 $.fn.ajaxForm = function(options) {
377         return this.ajaxFormUnbind().bind('submit.form-plugin', function() {
378                 $(this).ajaxSubmit(options);
379                 return false;
380         }).bind('click.form-plugin', function(e) {
381                 var $el = $(e.target);
382                 if (!($el.is(":submit,input:image"))) {
383                         return;
384                 }
385                 var form = this;
386                 form.clk = e.target;
387                 if (e.target.type == 'image') {
388                         if (e.offsetX != undefined) {
389                                 form.clk_x = e.offsetX;
390                                 form.clk_y = e.offsetY;
391                         } else if (typeof $.fn.offset == 'function') { // try to use dimensions plugin
392                                 var offset = $el.offset();
393                                 form.clk_x = e.pageX - offset.left;
394                                 form.clk_y = e.pageY - offset.top;
395                         } else {
396                                 form.clk_x = e.pageX - e.target.offsetLeft;
397                                 form.clk_y = e.pageY - e.target.offsetTop;
398                         }
399                 }
400                 // clear form vars
401                 setTimeout(function() { form.clk = form.clk_x = form.clk_y = null; }, 10);
402         });
403 };
404
405 // ajaxFormUnbind unbinds the event handlers that were bound by ajaxForm
406 $.fn.ajaxFormUnbind = function() {
407         return this.unbind('submit.form-plugin click.form-plugin');
408 };
409
410 /**
411  * formToArray() gathers form element data into an array of objects that can
412  * be passed to any of the following ajax functions: $.get, $.post, or load.
413  * Each object in the array has both a 'name' and 'value' property.  An example of
414  * an array for a simple login form might be:
415  *
416  * [ { name: 'username', value: 'jresig' }, { name: 'password', value: 'secret' } ]
417  *
418  * It is this array that is passed to pre-submit callback functions provided to the
419  * ajaxSubmit() and ajaxForm() methods.
420  */
421 $.fn.formToArray = function(semantic) {
422         var a = [];
423         if (this.length == 0) return a;
424
425         var form = this[0];
426         var els = semantic ? form.getElementsByTagName('*') : form.elements;
427         if (!els) return a;
428         for(var i=0, max=els.length; i < max; i++) {
429                 var el = els[i];
430                 var n = el.name;
431                 if (!n) continue;
432
433                 if (semantic && form.clk && el.type == "image") {
434                         // handle image inputs on the fly when semantic == true
435                         if(!el.disabled && form.clk == el) {
436                                 a.push({name: n, value: $(el).val()});
437                                 a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
438                         }
439                         continue;
440                 }
441
442                 var v = $.fieldValue(el, true);
443                 if (v && v.constructor == Array) {
444                         for(var j=0, jmax=v.length; j < jmax; j++)
445                                 a.push({name: n, value: v[j]});
446                 }
447                 else if (v !== null && typeof v != 'undefined')
448                         a.push({name: n, value: v});
449         }
450
451         if (!semantic && form.clk) {
452                 // input type=='image' are not found in elements array! handle it here
453                 var $input = $(form.clk), input = $input[0], n = input.name;
454                 if (n && !input.disabled && input.type == 'image') {
455                         a.push({name: n, value: $input.val()});
456                         a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
457                 }
458         }
459         return a;
460 };
461
462 /**
463  * Serializes form data into a 'submittable' string. This method will return a string
464  * in the format: name1=value1&amp;name2=value2
465  */
466 $.fn.formSerialize = function(semantic) {
467         //hand off to jQuery.param for proper encoding
468         return $.param(this.formToArray(semantic));
469 };
470
471 /**
472  * Serializes all field elements in the jQuery object into a query string.
473  * This method will return a string in the format: name1=value1&amp;name2=value2
474  */
475 $.fn.fieldSerialize = function(successful) {
476         var a = [];
477         this.each(function() {
478                 var n = this.name;
479                 if (!n) return;
480                 var v = $.fieldValue(this, successful);
481                 if (v && v.constructor == Array) {
482                         for (var i=0,max=v.length; i < max; i++)
483                                 a.push({name: n, value: v[i]});
484                 }
485                 else if (v !== null && typeof v != 'undefined')
486                         a.push({name: this.name, value: v});
487         });
488         //hand off to jQuery.param for proper encoding
489         return $.param(a);
490 };
491
492 /**
493  * Returns the value(s) of the element in the matched set.  For example, consider the following form:
494  *
495  *  <form><fieldset>
496  *        <input name="A" type="text" />
497  *        <input name="A" type="text" />
498  *        <input name="B" type="checkbox" value="B1" />
499  *        <input name="B" type="checkbox" value="B2"/>
500  *        <input name="C" type="radio" value="C1" />
501  *        <input name="C" type="radio" value="C2" />
502  *  </fieldset></form>
503  *
504  *  var v = $(':text').fieldValue();
505  *  // if no values are entered into the text inputs
506  *  v == ['','']
507  *  // if values entered into the text inputs are 'foo' and 'bar'
508  *  v == ['foo','bar']
509  *
510  *  var v = $(':checkbox').fieldValue();
511  *  // if neither checkbox is checked
512  *  v === undefined
513  *  // if both checkboxes are checked
514  *  v == ['B1', 'B2']
515  *
516  *  var v = $(':radio').fieldValue();
517  *  // if neither radio is checked
518  *  v === undefined
519  *  // if first radio is checked
520  *  v == ['C1']
521  *
522  * The successful argument controls whether or not the field element must be 'successful'
523  * (per http://www.w3.org/TR/html4/interact/forms.html#successful-controls).
524  * The default value of the successful argument is true.  If this value is false the value(s)
525  * for each element is returned.
526  *
527  * Note: This method *always* returns an array.  If no valid value can be determined the
528  *         array will be empty, otherwise it will contain one or more values.
529  */
530 $.fn.fieldValue = function(successful) {
531         for (var val=[], i=0, max=this.length; i < max; i++) {
532                 var el = this[i];
533                 var v = $.fieldValue(el, successful);
534                 if (v === null || typeof v == 'undefined' || (v.constructor == Array && !v.length))
535                         continue;
536                 v.constructor == Array ? $.merge(val, v) : val.push(v);
537         }
538         return val;
539 };
540
541 /**
542  * Returns the value of the field element.
543  */
544 $.fieldValue = function(el, successful) {
545         var n = el.name, t = el.type, tag = el.tagName.toLowerCase();
546         if (typeof successful == 'undefined') successful = true;
547
548         if (successful && (!n || el.disabled || t == 'reset' || t == 'button' ||
549                 (t == 'checkbox' || t == 'radio') && !el.checked ||
550                 (t == 'submit' || t == 'image') && el.form && el.form.clk != el ||
551                 tag == 'select' && el.selectedIndex == -1))
552                         return null;
553
554         if (tag == 'select') {
555                 var index = el.selectedIndex;
556                 if (index < 0) return null;
557                 var a = [], ops = el.options;
558                 var one = (t == 'select-one');
559                 var max = (one ? index+1 : ops.length);
560                 for(var i=(one ? index : 0); i < max; i++) {
561                         var op = ops[i];
562                         if (op.selected) {
563                                 var v = op.value;
564                                 if (!v) // extra pain for IE...
565                                         v = (op.attributes && op.attributes['value'] && !(op.attributes['value'].specified)) ? op.text : op.value;
566                                 if (one) return v;
567                                 a.push(v);
568                         }
569                 }
570                 return a;
571         }
572         return el.value;
573 };
574
575 /**
576  * Clears the form data.  Takes the following actions on the form's input fields:
577  *  - input text fields will have their 'value' property set to the empty string
578  *  - select elements will have their 'selectedIndex' property set to -1
579  *  - checkbox and radio inputs will have their 'checked' property set to false
580  *  - inputs of type submit, button, reset, and hidden will *not* be effected
581  *  - button elements will *not* be effected
582  */
583 $.fn.clearForm = function() {
584         return this.each(function() {
585                 $('input,select,textarea', this).clearFields();
586         });
587 };
588
589 /**
590  * Clears the selected form elements.
591  */
592 $.fn.clearFields = $.fn.clearInputs = function() {
593         return this.each(function() {
594                 var t = this.type, tag = this.tagName.toLowerCase();
595                 if (t == 'text' || t == 'password' || tag == 'textarea')
596                         this.value = '';
597                 else if (t == 'checkbox' || t == 'radio')
598                         this.checked = false;
599                 else if (tag == 'select')
600                         this.selectedIndex = -1;
601         });
602 };
603
604 /**
605  * Resets the form data.  Causes all form elements to be reset to their original value.
606  */
607 $.fn.resetForm = function() {
608         return this.each(function() {
609                 // guard against an input with the name of 'reset'
610                 // note that IE reports the reset function as an 'object'
611                 if (typeof this.reset == 'function' || (typeof this.reset == 'object' && !this.reset.nodeType))
612                         this.reset();
613         });
614 };
615
616 /**
617  * Enables or disables any matching elements.
618  */
619 $.fn.enable = function(b) {
620         if (b == undefined) b = true;
621         return this.each(function() {
622                 this.disabled = !b;
623         });
624 };
625
626 /**
627  * Checks/unchecks any matching checkboxes or radio buttons and
628  * selects/deselects and matching option elements.
629  */
630 $.fn.selected = function(select) {
631         if (select == undefined) select = true;
632         return this.each(function() {
633                 var t = this.type;
634                 if (t == 'checkbox' || t == 'radio')
635                         this.checked = select;
636                 else if (this.tagName.toLowerCase() == 'option') {
637                         var $sel = $(this).parent('select');
638                         if (select && $sel[0] && $sel[0].type == 'select-one') {
639                                 // deselect all other options
640                                 $sel.find('option').selected(false);
641                         }
642                         this.selected = select;
643                 }
644         });
645 };
646
647 // helper fn for console logging
648 // set $.fn.ajaxSubmit.debug to true to enable debug logging
649 function log() {
650         if ($.fn.ajaxSubmit.debug && window.console && window.console.log)
651                 window.console.log('[jquery.form] ' + Array.prototype.join.call(arguments,''));
652 };
653
654 })(jQuery);