Browse Source

Evolution #90: Ajaxsification des actions

bastien 12 years ago
parent
commit
40d1f77e60

+ 3 - 3
app/Resources/translations/elements.fr.yml View File

@@ -3,9 +3,9 @@ noelements:
3 3
   sentence:             Aucun élément a afficher
4 4
   sentence_filter:      |
5 5
                          Aucun élément a afficher, ceci est peut-être du a un
6
-                         filtre trop restrictif.
7
-                         <a class="filter_clear_url" href="#" >%link_string%</a> ?
8
-  sentence_filter_link_string: Vider le filtre
6
+                         filtre trop restrictif.<br />Vous pouvez ajouter des tags ou
7
+                         <a class="filter_clear_url" href="#" >%link_string%</a>.
8
+  sentence_filter_link_string: les retirer
9 9
   
10 10
 element:
11 11
   favorite:

+ 1 - 1
src/Muzich/CoreBundle/Controller/CoreController.php View File

@@ -206,7 +206,7 @@ class CoreController extends Controller
206 206
     {
207 207
       if ($this->getRequest()->isXmlHttpRequest())
208 208
       {
209
-
209
+        return $this->jsonResponse(array(true));
210 210
       }
211 211
       else
212 212
       {

+ 32 - 9
src/Muzich/CoreBundle/Controller/SearchController.php View File

@@ -12,12 +12,11 @@ use Symfony\Component\HttpFoundation\Response;
12 12
 class SearchController extends Controller
13 13
 {
14 14
   
15
-  protected function searchElementsMore($elements, $invertcolors)
15
+  protected function searchElementsMore($elements, $invertcolors, $message)
16 16
   {
17 17
     
18 18
     $end = (($count = count($elements)) < $this->container->getParameter('search_ajax_more'));
19 19
     $html = '';
20
-    $message = '';
21 20
     if ($count)
22 21
     {
23 22
       $html = $this->render('MuzichCoreBundle:SearchElement:default.html.twig', array(
@@ -27,11 +26,6 @@ class SearchController extends Controller
27 26
       ))->getContent();
28 27
     }
29 28
     
30
-    if (!$count || $end)
31
-    {
32
-      $message = $this->trans('elements.ajax.more.noelements', array(), 'elements');
33
-    }
34
-    
35 29
     return $this->jsonResponse(array(
36 30
       'count'   => $count,
37 31
       'message' => $message,
@@ -52,8 +46,10 @@ class SearchController extends Controller
52 46
     
53 47
     $search_form = $this->getSearchForm($search_object);
54 48
     
49
+    $form_submited = false;
55 50
     if ($request->getMethod() == 'POST')
56 51
     {
52
+      $form_submited = true;
57 53
       $search_form->bindRequest($request);
58 54
       // Si le formulaire est valide
59 55
       if ($search_form->isValid())
@@ -75,6 +71,27 @@ class SearchController extends Controller
75 71
     
76 72
     if ($this->getRequest()->isXmlHttpRequest())
77 73
     {
74
+      if ($form_submited)
75
+      {
76
+        $message = $this->trans(
77
+          'noelements.sentence_filter',
78
+          array('%link_string%' => $this->trans(
79
+            'noelements.sentence_filter_link_string',
80
+            array(),
81
+            'elements'
82
+          )),
83
+          'elements'
84
+        );
85
+      }
86
+      else
87
+      {
88
+        $message = $this->trans(
89
+          'elements.ajax.more.noelements', 
90
+          array(), 
91
+          'elements'
92
+        );
93
+      }
94
+      
78 95
       // template qui apelle doSearchElementsAction 
79 96
       $search = $this->getElementSearcher();
80 97
       $search->update(array(
@@ -83,7 +100,7 @@ class SearchController extends Controller
83 100
       ));
84 101
       $elements = $search->getElements($this->getDoctrine(), $this->getUserId());
85 102
       
86
-      return $this->searchElementsMore($elements, $invertcolors);
103
+      return $this->searchElementsMore($elements, $invertcolors, $message);      
87 104
     }
88 105
     else
89 106
     {
@@ -127,7 +144,13 @@ class SearchController extends Controller
127 144
 
128 145
       $elements = $search->getElements($this->getDoctrine(), $this->getUserId());
129 146
       
130
-      return $this->searchElementsMore($elements, $invertcolors);
147
+      return $this->searchElementsMore($elements, $invertcolors,
148
+        $this->trans(
149
+          'elements.ajax.more.noelements', 
150
+          array(), 
151
+          'elements'
152
+        )
153
+      );
131 154
     }
132 155
     
133 156
     throw new \Exception('XmlHttpRequest only for this action');

+ 2 - 0
src/Muzich/CoreBundle/Resources/views/SearchElement/default.html.twig View File

@@ -100,6 +100,8 @@
100 100
 
101 101
 {% else %}
102 102
 
103
+  <ul class="elements"></ul>
104
+
103 105
   <div class="no_elements">
104 106
     {% if noelements_filter is defined %}
105 107
       <p class="no-elements">

+ 1 - 0
src/Muzich/CoreBundle/Resources/views/layout.html.twig View File

@@ -21,6 +21,7 @@
21 21
   <script src="{{ asset('js/tags/jquery.autoGrowInput.js') }}" type="text/javascript"></script>
22 22
   <script src="{{ asset('js/tags/jquery.tagBox.js') }}" type="text/javascript"></script>
23 23
   <script src="{{ asset('js/formdefault/jquery.formdefaults.js') }}" type="text/javascript"></script>
24
+  <script src="{{ asset('js/jquery.form-2.14.js') }}" type="text/javascript"></script>
24 25
   {% block js %}{% endblock %}
25 26
   
26 27
 </head>

+ 3 - 1
src/Muzich/HomeBundle/Resources/views/Home/index.html.twig View File

@@ -38,6 +38,9 @@
38 38
 
39 39
   {% include "MuzichCoreBundle:SearchElement:default.html.twig" with {'noelements_filter' : true }%}
40 40
     
41
+  <div class="elements_loader_div">
42
+    <img class="elements_more_loader" style="display: none;" src="{{ asset('/bundles/muzichcore/img/ajax-loader.gif') }}" alt="loading" />
43
+  </div>
41 44
   
42 45
   {% if more_count is defined %} 
43 46
   {% if elements|length %}
@@ -46,7 +49,6 @@
46 49
          {{ 'more'|trans({}, 'userui') }}
47 50
        </a>
48 51
      </span>
49
-     <img class="elements_more_loader" style="display: none;" src="{{ asset('/bundles/muzichcore/img/ajax-loader.gif') }}" alt="loading" />
50 52
   {% endif %}
51 53
   {% endif %}
52 54
   

+ 4 - 1
src/Muzich/HomeBundle/Resources/views/Show/showGroup.html.twig View File

@@ -51,6 +51,10 @@
51 51
   
52 52
   {% include "MuzichCoreBundle:SearchElement:default.html.twig" with {'no_group_name' : true} %}
53 53
     
54
+  <div class="elements_loader_div">
55
+    <img class="elements_more_loader" style="display: none;" src="{{ asset('/bundles/muzichcore/img/ajax-loader.gif') }}" alt="loading" />
56
+  </div>
57
+    
54 58
   {% if more_count is defined %} 
55 59
   {% if elements|length %}
56 60
    <span class="elements_more">
@@ -61,7 +65,6 @@
61 65
        {{ 'more'|trans({}, 'userui') }}
62 66
      </a>
63 67
    </span>
64
-   <img class="elements_more_loader" style="display: none;" src="{{ asset('/bundles/muzichcore/img/ajax-loader.gif') }}" alt="loading" />
65 68
   {% endif %}
66 69
   {% endif %}
67 70
   

+ 4 - 1
src/Muzich/HomeBundle/Resources/views/Show/showUser.html.twig View File

@@ -25,6 +25,10 @@
25 25
   
26 26
   {% include "MuzichCoreBundle:SearchElement:default.html.twig" %}
27 27
     
28
+  <div class="elements_loader_div">
29
+    <img class="elements_more_loader" style="display: none;" src="{{ asset('/bundles/muzichcore/img/ajax-loader.gif') }}" alt="loading" />
30
+  </div>
31
+  
28 32
   {% if more_count is defined %} 
29 33
   {% if elements|length %}
30 34
    <span class="elements_more">
@@ -35,7 +39,6 @@
35 39
        {{ 'more'|trans({}, 'userui') }}
36 40
      </a>
37 41
    </span>
38
-   <img class="elements_more_loader" style="display: none;" src="{{ asset('/bundles/muzichcore/img/ajax-loader.gif') }}" alt="loading" />
39 42
   {% endif %}
40 43
   {% endif %}
41 44
   

+ 11 - 1
web/bundles/muzichcore/css/main.css View File

@@ -579,4 +579,14 @@ div.search_tag_list ul.search_tag_list li:hover
579 579
 {
580 580
   color: black;
581 581
 }
582
- 
582
+ 
583
+.elements_loader_div
584
+{
585
+  text-align: center;
586
+}
587
+ 
588
+.elements_loader_div img
589
+{
590
+  margin-left: auto;
591
+  margin-right: auto;
592
+}

+ 64 - 40
web/bundles/muzichcore/js/muzich.js View File

@@ -161,53 +161,45 @@ function str_replace (search, replace, subject, count) {
161 161
 }
162 162
 
163 163
 $(document).ready(function(){
164
+    
164 165
   
165 166
   // Bouton de personalisation du filtre
166 167
   // pour le moment ce ne sotn que des redirection vers des actions
167
-  $('.tags_prompt input.clear, .filter_clear_url').click(function(){
168
+  $('.tags_prompt input.clear, a.filter_clear_url').live("click", function(){
168 169
     $(location).attr('href', $('input.filter_clear_url').val());
169 170
   });
170
-  $('.tags_prompt input.mytags').click(function(){
171
+  $('.tags_prompt input.mytags').live("click", function(){
171 172
     $(location).attr('href', $('input.filter_mytags_url').val());
172 173
   });
173
-  
174
-  // On met les listeners lié aux éléments de façon a pouvoir écouter
175
-  // les événenements des éléments chargés en ajax en appelelant
176
-  // cette fonction après un ptit coup d'ajax
177
-  function init_elements()
178
-  {
179
-  
180
-    // Affichage un/des embed
181
-    $('a.element_embed_open_link').click(function(){
182
-       $(this).parent().parent('li.element').find('a.element_embed_open_link').hide();
183
-       $(this).parent().parent('li.element').find('a.element_embed_close_link').show();
184
-       $(this).parent().parent('li.element').find('div.element_embed').show();
185
-       return false;
186
-    });
187
-
188
-    // Fermeture du embed si demandé
189
-    $('a.element_embed_close_link').click(function(){
190
-       $(this).parent().parent('li.element').find('a.element_embed_open_link').show();
191
-       $(this).parent().parent('li.element').find('a.element_embed_close_link').hide();
192
-       $(this).parent().parent('li.element').find('div.element_embed').hide();
193
-       return false;
194
-    });
195
-
196
-    // Mise en favoris
197
-    $('a.favorite_link').click(function(){
198
-       link = $(this);
199
-       $.getJSON($(this).attr('href'), function(response) {
200
-         img = link.find('img');
201
-         link.attr('href', response.link_new_url);
202
-         img.attr('src', response.img_new_src);
203
-         img.attr('title', response.img_new_title);
204
-       });
205
-       return false;
206
-    });
174
+
175
+  // Affichage un/des embed
176
+  $('a.element_embed_open_link').live("click", function(){
177
+     $(this).parent().parent('li.element').find('a.element_embed_open_link').hide();
178
+     $(this).parent().parent('li.element').find('a.element_embed_close_link').show();
179
+     $(this).parent().parent('li.element').find('div.element_embed').show();
180
+     return false;
181
+  });
182
+
183
+  // Fermeture du embed si demandé
184
+  $('a.element_embed_close_link').live("click", function(){
185
+     $(this).parent().parent('li.element').find('a.element_embed_open_link').show();
186
+     $(this).parent().parent('li.element').find('a.element_embed_close_link').hide();
187
+     $(this).parent().parent('li.element').find('div.element_embed').hide();
188
+     return false;
189
+  });
190
+
191
+  // Mise en favoris
192
+  $('a.favorite_link').live("click", function(){
193
+     link = $(this);
194
+     $.getJSON($(this).attr('href'), function(response) {
195
+       img = link.find('img');
196
+       link.attr('href', response.link_new_url);
197
+       img.attr('src', response.img_new_src);
198
+       img.attr('title', response.img_new_title);
199
+     });
200
+     return false;
201
+  });
207 202
    
208
-  }
209
-  
210
-  init_elements();
211 203
    
212 204
    // Plus d'éléments
213 205
    last_id = null;
@@ -226,7 +218,6 @@ $(document).ready(function(){
226 218
        {
227 219
          $('ul.elements').append(response.html);
228 220
          $('img.elements_more_loader').hide();
229
-         init_elements();
230 221
        }
231 222
        
232 223
        if (response.end || response.count < 1)
@@ -240,6 +231,39 @@ $(document).ready(function(){
240 231
      return false;
241 232
    });
242 233
    
234
+  tag_box_input_value = $('ul.tagbox input[type="text"]').val();
235
+   
236
+  // Filtre et affichage éléments ajax
237
+  $('form[name="element_search_form"] input[type="submit"]').click(function(){
238
+    $('ul.elements').html('');
239
+    $('div.no_elements').hide();
240
+    $('img.elements_more_loader').show();
241
+  });
242
+  
243
+  $('form[name="element_search_form"]').ajaxForm(function(response) { 
244
+    
245
+    $('ul.elements').html(response.html);
246
+    
247
+    if (response.count)
248
+     {
249
+       $('img.elements_more_loader').hide();
250
+       $('span.elements_more').show();
251
+       $('a.elements_more').show();
252
+     }
253
+
254
+     if (response.count < 1)
255
+     {
256
+       $('img.elements_more_loader').hide();
257
+       $('ul.elements').after('<div class="no_elements"><p class="no-elements">'+
258
+         response.message+'</p></div>');
259
+         $('a.elements_more').hide()
260
+       ;
261
+     }
262
+     
263
+     $('ul.tagbox input[type="text"]').val(tag_box_input_value);
264
+    
265
+  }); 
266
+   
243 267
  });
244 268
  
245 269
  

+ 980 - 0
web/js/jquery.form-2.14.js View File

@@ -0,0 +1,980 @@
1
+/*!
2
+ * jQuery Form Plugin
3
+ * version: 2.94 (13-DEC-2011)
4
+ * @requires jQuery v1.3.2 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(e) {
22
+			e.preventDefault(); // <-- important
23
+			$(this).ajaxSubmit({
24
+				target: '#output'
25
+			});
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
+	var method, action, url, $form = this;
54
+
55
+	if (typeof options == 'function') {
56
+		options = { success: options };
57
+	}
58
+
59
+	method = this.attr('method');
60
+	action = this.attr('action');
61
+	url = (typeof action === 'string') ? $.trim(action) : '';
62
+	url = url || window.location.href || '';
63
+	if (url) {
64
+		// clean url (don't include hash vaue)
65
+		url = (url.match(/^([^#]+)/)||[])[1];
66
+	}
67
+
68
+	options = $.extend(true, {
69
+		url:  url,
70
+		success: $.ajaxSettings.success,
71
+		type: method || 'GET',
72
+		iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank'
73
+	}, options);
74
+
75
+	// hook for manipulating the form data before it is extracted;
76
+	// convenient for use with rich editors like tinyMCE or FCKEditor
77
+	var veto = {};
78
+	this.trigger('form-pre-serialize', [this, options, veto]);
79
+	if (veto.veto) {
80
+		log('ajaxSubmit: submit vetoed via form-pre-serialize trigger');
81
+		return this;
82
+	}
83
+
84
+	// provide opportunity to alter form data before it is serialized
85
+	if (options.beforeSerialize && options.beforeSerialize(this, options) === false) {
86
+		log('ajaxSubmit: submit aborted via beforeSerialize callback');
87
+		return this;
88
+	}
89
+
90
+	var traditional = options.traditional;
91
+	if ( traditional === undefined ) {
92
+		traditional = $.ajaxSettings.traditional;
93
+	}
94
+	
95
+	var qx,n,v,a = this.formToArray(options.semantic);
96
+	if (options.data) {
97
+		options.extraData = options.data;
98
+		qx = $.param(options.data, traditional);
99
+	}
100
+
101
+	// give pre-submit callback an opportunity to abort the submit
102
+	if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) {
103
+		log('ajaxSubmit: submit aborted via beforeSubmit callback');
104
+		return this;
105
+	}
106
+
107
+	// fire vetoable 'validate' event
108
+	this.trigger('form-submit-validate', [a, this, options, veto]);
109
+	if (veto.veto) {
110
+		log('ajaxSubmit: submit vetoed via form-submit-validate trigger');
111
+		return this;
112
+	}
113
+
114
+	var q = $.param(a, traditional);
115
+	if (qx) {
116
+		q = ( q ? (q + '&' + qx) : qx );
117
+	}	
118
+	if (options.type.toUpperCase() == 'GET') {
119
+		options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q;
120
+		options.data = null;  // data is null for 'get'
121
+	}
122
+	else {
123
+		options.data = q; // data is the query string for 'post'
124
+	}
125
+
126
+	var callbacks = [];
127
+	if (options.resetForm) {
128
+		callbacks.push(function() { $form.resetForm(); });
129
+	}
130
+	if (options.clearForm) {
131
+		callbacks.push(function() { $form.clearForm(options.includeHidden); });
132
+	}
133
+
134
+	// perform a load on the target only if dataType is not provided
135
+	if (!options.dataType && options.target) {
136
+		var oldSuccess = options.success || function(){};
137
+		callbacks.push(function(data) {
138
+			var fn = options.replaceTarget ? 'replaceWith' : 'html';
139
+			$(options.target)[fn](data).each(oldSuccess, arguments);
140
+		});
141
+	}
142
+	else if (options.success) {
143
+		callbacks.push(options.success);
144
+	}
145
+
146
+	options.success = function(data, status, xhr) { // jQuery 1.4+ passes xhr as 3rd arg
147
+		var context = options.context || options;	// jQuery 1.4+ supports scope context 
148
+		for (var i=0, max=callbacks.length; i < max; i++) {
149
+			callbacks[i].apply(context, [data, status, xhr || $form, $form]);
150
+		}
151
+	};
152
+
153
+	// are there files to upload?
154
+	var fileInputs = $('input:file:enabled[value]', this); // [value] (issue #113)
155
+	var hasFileInputs = fileInputs.length > 0;
156
+	var mp = 'multipart/form-data';
157
+	var multipart = ($form.attr('enctype') == mp || $form.attr('encoding') == mp);
158
+
159
+	var fileAPI = !!(hasFileInputs && fileInputs.get(0).files && window.FormData);
160
+	log("fileAPI :" + fileAPI);
161
+	var shouldUseFrame = (hasFileInputs || multipart) && !fileAPI;
162
+
163
+	// options.iframe allows user to force iframe mode
164
+	// 06-NOV-09: now defaulting to iframe mode if file input is detected
165
+	if (options.iframe !== false && (options.iframe || shouldUseFrame)) {
166
+		// hack to fix Safari hang (thanks to Tim Molendijk for this)
167
+		// see:  http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d
168
+		if (options.closeKeepAlive) {
169
+			$.get(options.closeKeepAlive, function() {
170
+				fileUploadIframe(a);
171
+			});
172
+		}
173
+  		else {
174
+			fileUploadIframe(a);
175
+  		}
176
+	}
177
+	else if ((hasFileInputs || multipart) && fileAPI) {
178
+		options.progress = options.progress || $.noop;
179
+		fileUploadXhr(a);
180
+	}
181
+	else {
182
+		$.ajax(options);
183
+	}
184
+
185
+	 // fire 'notify' event
186
+	 this.trigger('form-submit-notify', [this, options]);
187
+	 return this;
188
+
189
+	 // XMLHttpRequest Level 2 file uploads (big hat tip to francois2metz)
190
+	function fileUploadXhr(a) {
191
+		var formdata = new FormData();
192
+
193
+		for (var i=0; i < a.length; i++) {
194
+			if (a[i].type == 'file')
195
+				continue;
196
+			formdata.append(a[i].name, a[i].value);
197
+		}
198
+
199
+		$form.find('input:file:enabled').each(function(){
200
+			var name = $(this).attr('name'), files = this.files;
201
+			if (name) {
202
+				for (var i=0; i < files.length; i++)
203
+					formdata.append(name, files[i]);
204
+			}
205
+		});
206
+
207
+		if (options.extraData) {
208
+			for (var k in options.extraData)
209
+				formdata.append(k, options.extraData[k])
210
+		}
211
+
212
+		options.data = null;
213
+
214
+		var s = $.extend(true, {}, $.ajaxSettings, options, {
215
+			contentType: false,
216
+			processData: false,
217
+			cache: false,
218
+			type: 'POST'
219
+		});
220
+
221
+      s.context = s.context || s;
222
+
223
+      s.data = null;
224
+      var beforeSend = s.beforeSend;
225
+      s.beforeSend = function(xhr, o) {
226
+          o.data = formdata;
227
+          if(xhr.upload) { // unfortunately, jQuery doesn't expose this prop (http://bugs.jquery.com/ticket/10190)
228
+              xhr.upload.onprogress = function(event) {
229
+                  o.progress(event.position, event.total);
230
+              };
231
+          }
232
+          if(beforeSend)
233
+              beforeSend.call(o, xhr, options);
234
+      };
235
+      $.ajax(s);
236
+   }
237
+
238
+	// private function for handling file uploads (hat tip to YAHOO!)
239
+	function fileUploadIframe(a) {
240
+		var form = $form[0], el, i, s, g, id, $io, io, xhr, sub, n, timedOut, timeoutHandle;
241
+		var useProp = !!$.fn.prop;
242
+
243
+		if (a) {
244
+			if ( useProp ) {
245
+				// ensure that every serialized input is still enabled
246
+				for (i=0; i < a.length; i++) {
247
+					el = $(form[a[i].name]);
248
+					el.prop('disabled', false);
249
+				}
250
+			} else {
251
+				for (i=0; i < a.length; i++) {
252
+					el = $(form[a[i].name]);
253
+					el.removeAttr('disabled');
254
+				}
255
+			};
256
+		}
257
+
258
+		if ($(':input[name=submit],:input[id=submit]', form).length) {
259
+			// if there is an input with a name or id of 'submit' then we won't be
260
+			// able to invoke the submit fn on the form (at least not x-browser)
261
+			alert('Error: Form elements must not have name or id of "submit".');
262
+			return;
263
+		}
264
+		
265
+		s = $.extend(true, {}, $.ajaxSettings, options);
266
+		s.context = s.context || s;
267
+		id = 'jqFormIO' + (new Date().getTime());
268
+		if (s.iframeTarget) {
269
+			$io = $(s.iframeTarget);
270
+			n = $io.attr('name');
271
+			if (n == null)
272
+			 	$io.attr('name', id);
273
+			else
274
+				id = n;
275
+		}
276
+		else {
277
+			$io = $('<iframe name="' + id + '" src="'+ s.iframeSrc +'" />');
278
+			$io.css({ position: 'absolute', top: '-1000px', left: '-1000px' });
279
+		}
280
+		io = $io[0];
281
+
282
+
283
+		xhr = { // mock object
284
+			aborted: 0,
285
+			responseText: null,
286
+			responseXML: null,
287
+			status: 0,
288
+			statusText: 'n/a',
289
+			getAllResponseHeaders: function() {},
290
+			getResponseHeader: function() {},
291
+			setRequestHeader: function() {},
292
+			abort: function(status) {
293
+				var e = (status === 'timeout' ? 'timeout' : 'aborted');
294
+				log('aborting upload... ' + e);
295
+				this.aborted = 1;
296
+				$io.attr('src', s.iframeSrc); // abort op in progress
297
+				xhr.error = e;
298
+				s.error && s.error.call(s.context, xhr, e, status);
299
+				g && $.event.trigger("ajaxError", [xhr, s, e]);
300
+				s.complete && s.complete.call(s.context, xhr, e);
301
+			}
302
+		};
303
+
304
+		g = s.global;
305
+		// trigger ajax global events so that activity/block indicators work like normal
306
+		if (g && ! $.active++) {
307
+			$.event.trigger("ajaxStart");
308
+		}
309
+		if (g) {
310
+			$.event.trigger("ajaxSend", [xhr, s]);
311
+		}
312
+
313
+		if (s.beforeSend && s.beforeSend.call(s.context, xhr, s) === false) {
314
+			if (s.global) {
315
+				$.active--;
316
+			}
317
+			return;
318
+		}
319
+		if (xhr.aborted) {
320
+			return;
321
+		}
322
+
323
+		// add submitting element to data if we know it
324
+		sub = form.clk;
325
+		if (sub) {
326
+			n = sub.name;
327
+			if (n && !sub.disabled) {
328
+				s.extraData = s.extraData || {};
329
+				s.extraData[n] = sub.value;
330
+				if (sub.type == "image") {
331
+					s.extraData[n+'.x'] = form.clk_x;
332
+					s.extraData[n+'.y'] = form.clk_y;
333
+				}
334
+			}
335
+		}
336
+		
337
+		var CLIENT_TIMEOUT_ABORT = 1;
338
+		var SERVER_ABORT = 2;
339
+
340
+		function getDoc(frame) {
341
+			var doc = frame.contentWindow ? frame.contentWindow.document : frame.contentDocument ? frame.contentDocument : frame.document;
342
+			return doc;
343
+		}
344
+		
345
+		// Rails CSRF hack (thanks to Yvan Barthelemy)
346
+		var csrf_token = $('meta[name=csrf-token]').attr('content');
347
+		var csrf_param = $('meta[name=csrf-param]').attr('content');
348
+		if (csrf_param && csrf_token) {
349
+			s.extraData = s.extraData || {};
350
+			s.extraData[csrf_param] = csrf_token;
351
+		}
352
+
353
+		// take a breath so that pending repaints get some cpu time before the upload starts
354
+		function doSubmit() {
355
+			// make sure form attrs are set
356
+			var t = $form.attr('target'), a = $form.attr('action');
357
+
358
+			// update form attrs in IE friendly way
359
+			form.setAttribute('target',id);
360
+			if (!method) {
361
+				form.setAttribute('method', 'POST');
362
+			}
363
+			if (a != s.url) {
364
+				form.setAttribute('action', s.url);
365
+			}
366
+
367
+			// ie borks in some cases when setting encoding
368
+			if (! s.skipEncodingOverride && (!method || /post/i.test(method))) {
369
+				$form.attr({
370
+					encoding: 'multipart/form-data',
371
+					enctype:  'multipart/form-data'
372
+				});
373
+			}
374
+
375
+			// support timout
376
+			if (s.timeout) {
377
+				timeoutHandle = setTimeout(function() { timedOut = true; cb(CLIENT_TIMEOUT_ABORT); }, s.timeout);
378
+			}
379
+			
380
+			// look for server aborts
381
+			function checkState() {
382
+				try {
383
+					var state = getDoc(io).readyState;
384
+					log('state = ' + state);
385
+					if (state.toLowerCase() == 'uninitialized')
386
+						setTimeout(checkState,50);
387
+				}
388
+				catch(e) {
389
+					log('Server abort: ' , e, ' (', e.name, ')');
390
+					cb(SERVER_ABORT);
391
+					timeoutHandle && clearTimeout(timeoutHandle);
392
+					timeoutHandle = undefined;
393
+				}
394
+			}
395
+
396
+			// add "extra" data to form if provided in options
397
+			var extraInputs = [];
398
+			try {
399
+				if (s.extraData) {
400
+					for (var n in s.extraData) {
401
+						extraInputs.push(
402
+							$('<input type="hidden" name="'+n+'">').attr('value',s.extraData[n])
403
+								.appendTo(form)[0]);
404
+					}
405
+				}
406
+
407
+				if (!s.iframeTarget) {
408
+					// add iframe to doc and submit the form
409
+					$io.appendTo('body');
410
+					io.attachEvent ? io.attachEvent('onload', cb) : io.addEventListener('load', cb, false);
411
+				}
412
+				setTimeout(checkState,15);
413
+				form.submit();
414
+			}
415
+			finally {
416
+				// reset attrs and remove "extra" input elements
417
+				form.setAttribute('action',a);
418
+				if(t) {
419
+					form.setAttribute('target', t);
420
+				} else {
421
+					$form.removeAttr('target');
422
+				}
423
+				$(extraInputs).remove();
424
+			}
425
+		}
426
+
427
+		if (s.forceSync) {
428
+			doSubmit();
429
+		}
430
+		else {
431
+			setTimeout(doSubmit, 10); // this lets dom updates render
432
+		}
433
+
434
+		var data, doc, domCheckCount = 50, callbackProcessed;
435
+
436
+		function cb(e) {
437
+			if (xhr.aborted || callbackProcessed) {
438
+				return;
439
+			}
440
+			try {
441
+				doc = getDoc(io);
442
+			}
443
+			catch(ex) {
444
+				log('cannot access response document: ', ex);
445
+				e = SERVER_ABORT;
446
+			}
447
+			if (e === CLIENT_TIMEOUT_ABORT && xhr) {
448
+				xhr.abort('timeout');
449
+				return;
450
+			}
451
+			else if (e == SERVER_ABORT && xhr) {
452
+				xhr.abort('server abort');
453
+				return;
454
+			}
455
+
456
+			if (!doc || doc.location.href == s.iframeSrc) {
457
+				// response not received yet
458
+				if (!timedOut)
459
+					return;
460
+			}
461
+			io.detachEvent ? io.detachEvent('onload', cb) : io.removeEventListener('load', cb, false);
462
+
463
+			var status = 'success', errMsg;
464
+			try {
465
+				if (timedOut) {
466
+					throw 'timeout';
467
+				}
468
+
469
+				var isXml = s.dataType == 'xml' || doc.XMLDocument || $.isXMLDoc(doc);
470
+				log('isXml='+isXml);
471
+				if (!isXml && window.opera && (doc.body == null || doc.body.innerHTML == '')) {
472
+					if (--domCheckCount) {
473
+						// in some browsers (Opera) the iframe DOM is not always traversable when
474
+						// the onload callback fires, so we loop a bit to accommodate
475
+						log('requeing onLoad callback, DOM not available');
476
+						setTimeout(cb, 250);
477
+						return;
478
+					}
479
+					// let this fall through because server response could be an empty document
480
+					//log('Could not access iframe DOM after mutiple tries.');
481
+					//throw 'DOMException: not available';
482
+				}
483
+
484
+				//log('response detected');
485
+				var docRoot = doc.body ? doc.body : doc.documentElement;
486
+				xhr.responseText = docRoot ? docRoot.innerHTML : null;
487
+				xhr.responseXML = doc.XMLDocument ? doc.XMLDocument : doc;
488
+				if (isXml)
489
+					s.dataType = 'xml';
490
+				xhr.getResponseHeader = function(header){
491
+					var headers = {'content-type': s.dataType};
492
+					return headers[header];
493
+				};
494
+				// support for XHR 'status' & 'statusText' emulation :
495
+				if (docRoot) {
496
+					xhr.status = Number( docRoot.getAttribute('status') ) || xhr.status;
497
+					xhr.statusText = docRoot.getAttribute('statusText') || xhr.statusText;
498
+				}
499
+
500
+				var dt = (s.dataType || '').toLowerCase();
501
+				var scr = /(json|script|text)/.test(dt);
502
+				if (scr || s.textarea) {
503
+					// see if user embedded response in textarea
504
+					var ta = doc.getElementsByTagName('textarea')[0];
505
+					if (ta) {
506
+						xhr.responseText = ta.value;
507
+						// support for XHR 'status' & 'statusText' emulation :
508
+						xhr.status = Number( ta.getAttribute('status') ) || xhr.status;
509
+						xhr.statusText = ta.getAttribute('statusText') || xhr.statusText;
510
+					}
511
+					else if (scr) {
512
+						// account for browsers injecting pre around json response
513
+						var pre = doc.getElementsByTagName('pre')[0];
514
+						var b = doc.getElementsByTagName('body')[0];
515
+						if (pre) {
516
+							xhr.responseText = pre.textContent ? pre.textContent : pre.innerText;
517
+						}
518
+						else if (b) {
519
+							xhr.responseText = b.textContent ? b.textContent : b.innerText;
520
+						}
521
+					}
522
+				}
523
+				else if (dt == 'xml' && !xhr.responseXML && xhr.responseText != null) {
524
+					xhr.responseXML = toXml(xhr.responseText);
525
+				}
526
+
527
+				try {
528
+					data = httpData(xhr, dt, s);
529
+				}
530
+				catch (e) {
531
+					status = 'parsererror';
532
+					xhr.error = errMsg = (e || status);
533
+				}
534
+			}
535
+			catch (e) {
536
+				log('error caught: ',e);
537
+				status = 'error';
538
+				xhr.error = errMsg = (e || status);
539
+			}
540
+
541
+			if (xhr.aborted) {
542
+				log('upload aborted');
543
+				status = null;
544
+			}
545
+
546
+			if (xhr.status) { // we've set xhr.status
547
+				status = (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) ? 'success' : 'error';
548
+			}
549
+
550
+			// ordering of these callbacks/triggers is odd, but that's how $.ajax does it
551
+			if (status === 'success') {
552
+				s.success && s.success.call(s.context, data, 'success', xhr);
553
+				g && $.event.trigger("ajaxSuccess", [xhr, s]);
554
+			}
555
+			else if (status) {
556
+				if (errMsg == undefined)
557
+					errMsg = xhr.statusText;
558
+				s.error && s.error.call(s.context, xhr, status, errMsg);
559
+				g && $.event.trigger("ajaxError", [xhr, s, errMsg]);
560
+			}
561
+
562
+			g && $.event.trigger("ajaxComplete", [xhr, s]);
563
+
564
+			if (g && ! --$.active) {
565
+				$.event.trigger("ajaxStop");
566
+			}
567
+
568
+			s.complete && s.complete.call(s.context, xhr, status);
569
+
570
+			callbackProcessed = true;
571
+			if (s.timeout)
572
+				clearTimeout(timeoutHandle);
573
+
574
+			// clean up
575
+			setTimeout(function() {
576
+				if (!s.iframeTarget)
577
+					$io.remove();
578
+				xhr.responseXML = null;
579
+			}, 100);
580
+		}
581
+
582
+		var toXml = $.parseXML || function(s, doc) { // use parseXML if available (jQuery 1.5+)
583
+			if (window.ActiveXObject) {
584
+				doc = new ActiveXObject('Microsoft.XMLDOM');
585
+				doc.async = 'false';
586
+				doc.loadXML(s);
587
+			}
588
+			else {
589
+				doc = (new DOMParser()).parseFromString(s, 'text/xml');
590
+			}
591
+			return (doc && doc.documentElement && doc.documentElement.nodeName != 'parsererror') ? doc : null;
592
+		};
593
+		var parseJSON = $.parseJSON || function(s) {
594
+			return window['eval']('(' + s + ')');
595
+		};
596
+
597
+		var httpData = function( xhr, type, s ) { // mostly lifted from jq1.4.4
598
+
599
+			var ct = xhr.getResponseHeader('content-type') || '',
600
+				xml = type === 'xml' || !type && ct.indexOf('xml') >= 0,
601
+				data = xml ? xhr.responseXML : xhr.responseText;
602
+
603
+			if (xml && data.documentElement.nodeName === 'parsererror') {
604
+				$.error && $.error('parsererror');
605
+			}
606
+			if (s && s.dataFilter) {
607
+				data = s.dataFilter(data, type);
608
+			}
609
+			if (typeof data === 'string') {
610
+				if (type === 'json' || !type && ct.indexOf('json') >= 0) {
611
+					data = parseJSON(data);
612
+				} else if (type === "script" || !type && ct.indexOf("javascript") >= 0) {
613
+					$.globalEval(data);
614
+				}
615
+			}
616
+			return data;
617
+		};
618
+	}
619
+};
620
+
621
+/**
622
+ * ajaxForm() provides a mechanism for fully automating form submission.
623
+ *
624
+ * The advantages of using this method instead of ajaxSubmit() are:
625
+ *
626
+ * 1: This method will include coordinates for <input type="image" /> elements (if the element
627
+ *	is used to submit the form).
628
+ * 2. This method will include the submit element's name/value data (for the element that was
629
+ *	used to submit the form).
630
+ * 3. This method binds the submit() method to the form for you.
631
+ *
632
+ * The options argument for ajaxForm works exactly as it does for ajaxSubmit.  ajaxForm merely
633
+ * passes the options argument along after properly binding events for submit elements and
634
+ * the form itself.
635
+ */
636
+$.fn.ajaxForm = function(options) {
637
+	// in jQuery 1.3+ we can fix mistakes with the ready state
638
+	if (this.length === 0) {
639
+		var o = { s: this.selector, c: this.context };
640
+		if (!$.isReady && o.s) {
641
+			log('DOM not ready, queuing ajaxForm');
642
+			$(function() {
643
+				$(o.s,o.c).ajaxForm(options);
644
+			});
645
+			return this;
646
+		}
647
+		// is your DOM ready?  http://docs.jquery.com/Tutorials:Introducing_$(document).ready()
648
+		log('terminating; zero elements found by selector' + ($.isReady ? '' : ' (DOM not ready)'));
649
+		return this;
650
+	}
651
+
652
+	return this.ajaxFormUnbind().bind('submit.form-plugin', function(e) {
653
+		if (!e.isDefaultPrevented()) { // if event has been canceled, don't proceed
654
+			e.preventDefault();
655
+			$(this).ajaxSubmit(options);
656
+		}
657
+	}).bind('click.form-plugin', function(e) {
658
+		var target = e.target;
659
+		var $el = $(target);
660
+		if (!($el.is(":submit,input:image"))) {
661
+			// is this a child element of the submit el?  (ex: a span within a button)
662
+			var t = $el.closest(':submit');
663
+			if (t.length == 0) {
664
+				return;
665
+			}
666
+			target = t[0];
667
+		}
668
+		var form = this;
669
+		form.clk = target;
670
+		if (target.type == 'image') {
671
+			if (e.offsetX != undefined) {
672
+				form.clk_x = e.offsetX;
673
+				form.clk_y = e.offsetY;
674
+			} else if (typeof $.fn.offset == 'function') { // try to use dimensions plugin
675
+				var offset = $el.offset();
676
+				form.clk_x = e.pageX - offset.left;
677
+				form.clk_y = e.pageY - offset.top;
678
+			} else {
679
+				form.clk_x = e.pageX - target.offsetLeft;
680
+				form.clk_y = e.pageY - target.offsetTop;
681
+			}
682
+		}
683
+		// clear form vars
684
+		setTimeout(function() { form.clk = form.clk_x = form.clk_y = null; }, 100);
685
+	});
686
+};
687
+
688
+// ajaxFormUnbind unbinds the event handlers that were bound by ajaxForm
689
+$.fn.ajaxFormUnbind = function() {
690
+	return this.unbind('submit.form-plugin click.form-plugin');
691
+};
692
+
693
+/**
694
+ * formToArray() gathers form element data into an array of objects that can
695
+ * be passed to any of the following ajax functions: $.get, $.post, or load.
696
+ * Each object in the array has both a 'name' and 'value' property.  An example of
697
+ * an array for a simple login form might be:
698
+ *
699
+ * [ { name: 'username', value: 'jresig' }, { name: 'password', value: 'secret' } ]
700
+ *
701
+ * It is this array that is passed to pre-submit callback functions provided to the
702
+ * ajaxSubmit() and ajaxForm() methods.
703
+ */
704
+$.fn.formToArray = function(semantic) {
705
+	var a = [];
706
+	if (this.length === 0) {
707
+		return a;
708
+	}
709
+
710
+	var form = this[0];
711
+	var els = semantic ? form.getElementsByTagName('*') : form.elements;
712
+	if (!els) {
713
+		return a;
714
+	}
715
+
716
+	var i,j,n,v,el,max,jmax;
717
+	for(i=0, max=els.length; i < max; i++) {
718
+		el = els[i];
719
+		n = el.name;
720
+		if (!n) {
721
+			continue;
722
+		}
723
+
724
+		if (semantic && form.clk && el.type == "image") {
725
+			// handle image inputs on the fly when semantic == true
726
+			if(!el.disabled && form.clk == el) {
727
+				a.push({name: n, value: $(el).val(), type: el.type });
728
+				a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
729
+			}
730
+			continue;
731
+		}
732
+
733
+		v = $.fieldValue(el, true);
734
+		if (v && v.constructor == Array) {
735
+			for(j=0, jmax=v.length; j < jmax; j++) {
736
+				a.push({name: n, value: v[j]});
737
+			}
738
+		}
739
+		else if (v !== null && typeof v != 'undefined') {
740
+			a.push({name: n, value: v, type: el.type});
741
+		}
742
+	}
743
+
744
+	if (!semantic && form.clk) {
745
+		// input type=='image' are not found in elements array! handle it here
746
+		var $input = $(form.clk), input = $input[0];
747
+		n = input.name;
748
+		if (n && !input.disabled && input.type == 'image') {
749
+			a.push({name: n, value: $input.val()});
750
+			a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
751
+		}
752
+	}
753
+	return a;
754
+};
755
+
756
+/**
757
+ * Serializes form data into a 'submittable' string. This method will return a string
758
+ * in the format: name1=value1&amp;name2=value2
759
+ */
760
+$.fn.formSerialize = function(semantic) {
761
+	//hand off to jQuery.param for proper encoding
762
+	return $.param(this.formToArray(semantic));
763
+};
764
+
765
+/**
766
+ * Serializes all field elements in the jQuery object into a query string.
767
+ * This method will return a string in the format: name1=value1&amp;name2=value2
768
+ */
769
+$.fn.fieldSerialize = function(successful) {
770
+	var a = [];
771
+	this.each(function() {
772
+		var n = this.name;
773
+		if (!n) {
774
+			return;
775
+		}
776
+		var v = $.fieldValue(this, successful);
777
+		if (v && v.constructor == Array) {
778
+			for (var i=0,max=v.length; i < max; i++) {
779
+				a.push({name: n, value: v[i]});
780
+			}
781
+		}
782
+		else if (v !== null && typeof v != 'undefined') {
783
+			a.push({name: this.name, value: v});
784
+		}
785
+	});
786
+	//hand off to jQuery.param for proper encoding
787
+	return $.param(a);
788
+};
789
+
790
+/**
791
+ * Returns the value(s) of the element in the matched set.  For example, consider the following form:
792
+ *
793
+ *  <form><fieldset>
794
+ *	  <input name="A" type="text" />
795
+ *	  <input name="A" type="text" />
796
+ *	  <input name="B" type="checkbox" value="B1" />
797
+ *	  <input name="B" type="checkbox" value="B2"/>
798
+ *	  <input name="C" type="radio" value="C1" />
799
+ *	  <input name="C" type="radio" value="C2" />
800
+ *  </fieldset></form>
801
+ *
802
+ *  var v = $(':text').fieldValue();
803
+ *  // if no values are entered into the text inputs
804
+ *  v == ['','']
805
+ *  // if values entered into the text inputs are 'foo' and 'bar'
806
+ *  v == ['foo','bar']
807
+ *
808
+ *  var v = $(':checkbox').fieldValue();
809
+ *  // if neither checkbox is checked
810
+ *  v === undefined
811
+ *  // if both checkboxes are checked
812
+ *  v == ['B1', 'B2']
813
+ *
814
+ *  var v = $(':radio').fieldValue();
815
+ *  // if neither radio is checked
816
+ *  v === undefined
817
+ *  // if first radio is checked
818
+ *  v == ['C1']
819
+ *
820
+ * The successful argument controls whether or not the field element must be 'successful'
821
+ * (per http://www.w3.org/TR/html4/interact/forms.html#successful-controls).
822
+ * The default value of the successful argument is true.  If this value is false the value(s)
823
+ * for each element is returned.
824
+ *
825
+ * Note: This method *always* returns an array.  If no valid value can be determined the
826
+ *	array will be empty, otherwise it will contain one or more values.
827
+ */
828
+$.fn.fieldValue = function(successful) {
829
+	for (var val=[], i=0, max=this.length; i < max; i++) {
830
+		var el = this[i];
831
+		var v = $.fieldValue(el, successful);
832
+		if (v === null || typeof v == 'undefined' || (v.constructor == Array && !v.length)) {
833
+			continue;
834
+		}
835
+		v.constructor == Array ? $.merge(val, v) : val.push(v);
836
+	}
837
+	return val;
838
+};
839
+
840
+/**
841
+ * Returns the value of the field element.
842
+ */
843
+$.fieldValue = function(el, successful) {
844
+	var n = el.name, t = el.type, tag = el.tagName.toLowerCase();
845
+	if (successful === undefined) {
846
+		successful = true;
847
+	}
848
+
849
+	if (successful && (!n || el.disabled || t == 'reset' || t == 'button' ||
850
+		(t == 'checkbox' || t == 'radio') && !el.checked ||
851
+		(t == 'submit' || t == 'image') && el.form && el.form.clk != el ||
852
+		tag == 'select' && el.selectedIndex == -1)) {
853
+			return null;
854
+	}
855
+
856
+	if (tag == 'select') {
857
+		var index = el.selectedIndex;
858
+		if (index < 0) {
859
+			return null;
860
+		}
861
+		var a = [], ops = el.options;
862
+		var one = (t == 'select-one');
863
+		var max = (one ? index+1 : ops.length);
864
+		for(var i=(one ? index : 0); i < max; i++) {
865
+			var op = ops[i];
866
+			if (op.selected) {
867
+				var v = op.value;
868
+				if (!v) { // extra pain for IE...
869
+					v = (op.attributes && op.attributes['value'] && !(op.attributes['value'].specified)) ? op.text : op.value;
870
+				}
871
+				if (one) {
872
+					return v;
873
+				}
874
+				a.push(v);
875
+			}
876
+		}
877
+		return a;
878
+	}
879
+	return $(el).val();
880
+};
881
+
882
+/**
883
+ * Clears the form data.  Takes the following actions on the form's input fields:
884
+ *  - input text fields will have their 'value' property set to the empty string
885
+ *  - select elements will have their 'selectedIndex' property set to -1
886
+ *  - checkbox and radio inputs will have their 'checked' property set to false
887
+ *  - inputs of type submit, button, reset, and hidden will *not* be effected
888
+ *  - button elements will *not* be effected
889
+ */
890
+$.fn.clearForm = function(includeHidden) {
891
+	return this.each(function() {
892
+		$('input,select,textarea', this).clearFields(includeHidden);
893
+	});
894
+};
895
+
896
+/**
897
+ * Clears the selected form elements.
898
+ */
899
+$.fn.clearFields = $.fn.clearInputs = function(includeHidden) {
900
+	var re = /^(?:color|date|datetime|email|month|number|password|range|search|tel|text|time|url|week)$/i; // 'hidden' is not in this list
901
+	return this.each(function() {
902
+		var t = this.type, tag = this.tagName.toLowerCase();
903
+		if (re.test(t) || tag == 'textarea' || (includeHidden && /hidden/.test(t)) ) {
904
+			this.value = '';
905
+		}
906
+		else if (t == 'checkbox' || t == 'radio') {
907
+			this.checked = false;
908
+		}
909
+		else if (tag == 'select') {
910
+			this.selectedIndex = -1;
911
+		}
912
+	});
913
+};
914
+
915
+/**
916
+ * Resets the form data.  Causes all form elements to be reset to their original value.
917
+ */
918
+$.fn.resetForm = function() {
919
+	return this.each(function() {
920
+		// guard against an input with the name of 'reset'
921
+		// note that IE reports the reset function as an 'object'
922
+		if (typeof this.reset == 'function' || (typeof this.reset == 'object' && !this.reset.nodeType)) {
923
+			this.reset();
924
+		}
925
+	});
926
+};
927
+
928
+/**
929
+ * Enables or disables any matching elements.
930
+ */
931
+$.fn.enable = function(b) {
932
+	if (b === undefined) {
933
+		b = true;
934
+	}
935
+	return this.each(function() {
936
+		this.disabled = !b;
937
+	});
938
+};
939
+
940
+/**
941
+ * Checks/unchecks any matching checkboxes or radio buttons and
942
+ * selects/deselects and matching option elements.
943
+ */
944
+$.fn.selected = function(select) {
945
+	if (select === undefined) {
946
+		select = true;
947
+	}
948
+	return this.each(function() {
949
+		var t = this.type;
950
+		if (t == 'checkbox' || t == 'radio') {
951
+			this.checked = select;
952
+		}
953
+		else if (this.tagName.toLowerCase() == 'option') {
954
+			var $sel = $(this).parent('select');
955
+			if (select && $sel[0] && $sel[0].type == 'select-one') {
956
+				// deselect all other options
957
+				$sel.find('option').selected(false);
958
+			}
959
+			this.selected = select;
960
+		}
961
+	});
962
+};
963
+
964
+// expose debug var
965
+$.fn.ajaxSubmit.debug = false;
966
+
967
+// helper fn for console logging
968
+function log() {
969
+	if (!$.fn.ajaxSubmit.debug) 
970
+		return;
971
+	var msg = '[jquery.form] ' + Array.prototype.join.call(arguments,'');
972
+	if (window.console && window.console.log) {
973
+		window.console.log(msg);
974
+	}
975
+	else if (window.opera && window.opera.postError) {
976
+		window.opera.postError(msg);
977
+	}
978
+};
979
+
980
+})(jQuery);