ajaxForm.js 20 KB

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