ajaxForm.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601
  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 = 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. var type = opts.dataTypep;
  221. var ct = xhr.getResponseHeader("content-type") || "",
  222. xml = type === "xml" || !type && ct.indexOf("xml") >= 0,
  223. data = xml ? xhr.responseXML : xhr.responseText;
  224. if ( xml && data.documentElement.nodeName === "parsererror" ) {
  225. jQuery.error( "parsererror" );
  226. }
  227. // Allow a pre-filtering function to sanitize the response
  228. // s is checked to keep backwards compatibility
  229. if ( s && s.dataFilter ) {
  230. data = s.dataFilter( data, type );
  231. }
  232. data = JSON.parse( data );
  233. }
  234. catch(e){
  235. console.log(e);
  236. ok = false;
  237. //$.handleError(opts, xhr, 'error', e);
  238. }
  239. // ordering of these callbacks/triggers is odd, but that's how $.ajax does it
  240. if (ok) {
  241. opts.success(data, 'success');
  242. if (g) $.event.trigger("ajaxSuccess", [xhr, opts]);
  243. }
  244. if (g) $.event.trigger("ajaxComplete", [xhr, opts]);
  245. if (g && ! --$.active) $.event.trigger("ajaxStop");
  246. if (opts.complete) opts.complete(xhr, ok ? 'success' : 'error');
  247. // clean up
  248. setTimeout(function() {
  249. $io.remove();
  250. xhr.responseXML = null;
  251. }, 100);
  252. };
  253. function toXml(s, doc) {
  254. if (window.ActiveXObject) {
  255. doc = new ActiveXObject('Microsoft.XMLDOM');
  256. doc.async = 'false';
  257. doc.loadXML(s);
  258. }
  259. else
  260. doc = (new DOMParser()).parseFromString(s, 'text/xml');
  261. return (doc && doc.documentElement && doc.documentElement.tagName != 'parsererror') ? doc : null;
  262. };
  263. };
  264. };
  265. /**
  266. * ajaxForm() provides a mechanism for fully automating form submission.
  267. *
  268. * The advantages of using this method instead of ajaxSubmit() are:
  269. *
  270. * 1: This method will include coordinates for <input type="image" /> elements (if the element
  271. * is used to submit the form).
  272. * 2. This method will include the submit element's name/value data (for the element that was
  273. * used to submit the form).
  274. * 3. This method binds the submit() method to the form for you.
  275. *
  276. * The options argument for ajaxForm works exactly as it does for ajaxSubmit. ajaxForm merely
  277. * passes the options argument along after properly binding events for submit elements and
  278. * the form itself.
  279. */
  280. $.fn.ajaxForm = function(options) {
  281. return this.ajaxFormUnbind().bind('submit.form-plugin',function() {
  282. $(this).ajaxSubmit(options);
  283. return false;
  284. }).each(function() {
  285. // store options in hash
  286. $(":submit,input:image", this).bind('click.form-plugin',function(e) {
  287. var form = this.form;
  288. form.clk = this;
  289. if (this.type == 'image') {
  290. if (e.offsetX != undefined) {
  291. form.clk_x = e.offsetX;
  292. form.clk_y = e.offsetY;
  293. } else if (typeof $.fn.offset == 'function') { // try to use dimensions plugin
  294. var offset = $(this).offset();
  295. form.clk_x = e.pageX - offset.left;
  296. form.clk_y = e.pageY - offset.top;
  297. } else {
  298. form.clk_x = e.pageX - this.offsetLeft;
  299. form.clk_y = e.pageY - this.offsetTop;
  300. }
  301. }
  302. // clear form vars
  303. setTimeout(function() { form.clk = form.clk_x = form.clk_y = null; }, 10);
  304. });
  305. });
  306. };
  307. // ajaxFormUnbind unbinds the event handlers that were bound by ajaxForm
  308. $.fn.ajaxFormUnbind = function() {
  309. this.unbind('submit.form-plugin');
  310. return this.each(function() {
  311. $(":submit,input:image", this).unbind('click.form-plugin');
  312. });
  313. };
  314. /**
  315. * formToArray() gathers form element data into an array of objects that can
  316. * be passed to any of the following ajax functions: $.get, $.post, or load.
  317. * Each object in the array has both a 'name' and 'value' property. An example of
  318. * an array for a simple login form might be:
  319. *
  320. * [ { name: 'username', value: 'jresig' }, { name: 'password', value: 'secret' } ]
  321. *
  322. * It is this array that is passed to pre-submit callback functions provided to the
  323. * ajaxSubmit() and ajaxForm() methods.
  324. */
  325. $.fn.formToArray = function(semantic) {
  326. var a = [];
  327. if (this.length == 0) return a;
  328. var form = this[0];
  329. var els = semantic ? form.getElementsByTagName('*') : form.elements;
  330. if (!els) return a;
  331. for(var i=0, max=els.length; i < max; i++) {
  332. var el = els[i];
  333. var n = el.name;
  334. if (!n) continue;
  335. if (semantic && form.clk && el.type == "image") {
  336. // handle image inputs on the fly when semantic == true
  337. if(!el.disabled && form.clk == el)
  338. a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
  339. continue;
  340. }
  341. var v = $.fieldValue(el, true);
  342. if (v && v.constructor == Array) {
  343. for(var j=0, jmax=v.length; j < jmax; j++)
  344. a.push({name: n, value: v[j]});
  345. }
  346. else if (v !== null && typeof v != 'undefined')
  347. a.push({name: n, value: v});
  348. }
  349. if (!semantic && form.clk) {
  350. // input type=='image' are not found in elements array! handle them here
  351. var inputs = form.getElementsByTagName("input");
  352. for(var i=0, max=inputs.length; i < max; i++) {
  353. var input = inputs[i];
  354. var n = input.name;
  355. if(n && !input.disabled && input.type == "image" && form.clk == input)
  356. a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
  357. }
  358. }
  359. return a;
  360. };
  361. /**
  362. * Serializes form data into a 'submittable' string. This method will return a string
  363. * in the format: name1=value1&amp;name2=value2
  364. */
  365. $.fn.formSerialize = function(semantic) {
  366. //hand off to jQuery.param for proper encoding
  367. return $.param(this.formToArray(semantic));
  368. };
  369. /**
  370. * Serializes all field elements in the jQuery object into a query string.
  371. * This method will return a string in the format: name1=value1&amp;name2=value2
  372. */
  373. $.fn.fieldSerialize = function(successful) {
  374. var a = [];
  375. this.each(function() {
  376. var n = this.name;
  377. if (!n) return;
  378. var v = $.fieldValue(this, successful);
  379. if (v && v.constructor == Array) {
  380. for (var i=0,max=v.length; i < max; i++)
  381. a.push({name: n, value: v[i]});
  382. }
  383. else if (v !== null && typeof v != 'undefined')
  384. a.push({name: this.name, value: v});
  385. });
  386. //hand off to jQuery.param for proper encoding
  387. return $.param(a);
  388. };
  389. /**
  390. * Returns the value(s) of the element in the matched set. For example, consider the following form:
  391. *
  392. * <form><fieldset>
  393. * <input name="A" type="text" />
  394. * <input name="A" type="text" />
  395. * <input name="B" type="checkbox" value="B1" />
  396. * <input name="B" type="checkbox" value="B2"/>
  397. * <input name="C" type="radio" value="C1" />
  398. * <input name="C" type="radio" value="C2" />
  399. * </fieldset></form>
  400. *
  401. * var v = $(':text').fieldValue();
  402. * // if no values are entered into the text inputs
  403. * v == ['','']
  404. * // if values entered into the text inputs are 'foo' and 'bar'
  405. * v == ['foo','bar']
  406. *
  407. * var v = $(':checkbox').fieldValue();
  408. * // if neither checkbox is checked
  409. * v === undefined
  410. * // if both checkboxes are checked
  411. * v == ['B1', 'B2']
  412. *
  413. * var v = $(':radio').fieldValue();
  414. * // if neither radio is checked
  415. * v === undefined
  416. * // if first radio is checked
  417. * v == ['C1']
  418. *
  419. * The successful argument controls whether or not the field element must be 'successful'
  420. * (per http://www.w3.org/TR/html4/interact/forms.html#successful-controls).
  421. * The default value of the successful argument is true. If this value is false the value(s)
  422. * for each element is returned.
  423. *
  424. * Note: This method *always* returns an array. If no valid value can be determined the
  425. * array will be empty, otherwise it will contain one or more values.
  426. */
  427. $.fn.fieldValue = function(successful) {
  428. for (var val=[], i=0, max=this.length; i < max; i++) {
  429. var el = this[i];
  430. var v = $.fieldValue(el, successful);
  431. if (v === null || typeof v == 'undefined' || (v.constructor == Array && !v.length))
  432. continue;
  433. v.constructor == Array ? $.merge(val, v) : val.push(v);
  434. }
  435. return val;
  436. };
  437. /**
  438. * Returns the value of the field element.
  439. */
  440. $.fieldValue = function(el, successful) {
  441. var n = el.name, t = el.type, tag = el.tagName.toLowerCase();
  442. if (typeof successful == 'undefined') successful = true;
  443. if (successful && (!n || el.disabled || t == 'reset' || t == 'button' ||
  444. (t == 'checkbox' || t == 'radio') && !el.checked ||
  445. (t == 'submit' || t == 'image') && el.form && el.form.clk != el ||
  446. tag == 'select' && el.selectedIndex == -1))
  447. return null;
  448. if (tag == 'select') {
  449. var index = el.selectedIndex;
  450. if (index < 0) return null;
  451. var a = [], ops = el.options;
  452. var one = (t == 'select-one');
  453. var max = (one ? index+1 : ops.length);
  454. for(var i=(one ? index : 0); i < max; i++) {
  455. var op = ops[i];
  456. if (op.selected) {
  457. var v = op.value;
  458. if (!v) // extra pain for IE...
  459. v = (op.attributes && op.attributes['value'] && !(op.attributes['value'].specified)) ? op.text : op.value;
  460. if (one) return v;
  461. a.push(v);
  462. }
  463. }
  464. return a;
  465. }
  466. return el.value;
  467. };
  468. /**
  469. * Clears the form data. Takes the following actions on the form's input fields:
  470. * - input text fields will have their 'value' property set to the empty string
  471. * - select elements will have their 'selectedIndex' property set to -1
  472. * - checkbox and radio inputs will have their 'checked' property set to false
  473. * - inputs of type submit, button, reset, and hidden will *not* be effected
  474. * - button elements will *not* be effected
  475. */
  476. $.fn.clearForm = function() {
  477. return this.each(function() {
  478. $('input,select,textarea', this).clearFields();
  479. });
  480. };
  481. /**
  482. * Clears the selected form elements.
  483. */
  484. $.fn.clearFields = $.fn.clearInputs = function() {
  485. return this.each(function() {
  486. var t = this.type, tag = this.tagName.toLowerCase();
  487. if (t == 'text' || t == 'password' || tag == 'textarea')
  488. this.value = '';
  489. else if (t == 'checkbox' || t == 'radio')
  490. this.checked = false;
  491. else if (tag == 'select')
  492. this.selectedIndex = -1;
  493. });
  494. };
  495. /**
  496. * Resets the form data. Causes all form elements to be reset to their original value.
  497. */
  498. $.fn.resetForm = function() {
  499. return this.each(function() {
  500. // guard against an input with the name of 'reset'
  501. // note that IE reports the reset function as an 'object'
  502. if (typeof this.reset == 'function' || (typeof this.reset == 'object' && !this.reset.nodeType))
  503. this.reset();
  504. });
  505. };
  506. /**
  507. * Enables or disables any matching elements.
  508. */
  509. $.fn.enable = function(b) {
  510. if (b == undefined) b = true;
  511. return this.each(function() {
  512. this.disabled = !b;
  513. });
  514. };
  515. /**
  516. * Checks/unchecks any matching checkboxes or radio buttons and
  517. * selects/deselects and matching option elements.
  518. */
  519. $.fn.selected = function(select) {
  520. if (select == undefined) select = true;
  521. return this.each(function() {
  522. var t = this.type;
  523. if (t == 'checkbox' || t == 'radio')
  524. this.checked = select;
  525. else if (this.tagName.toLowerCase() == 'option') {
  526. var $sel = $(this).parent('select');
  527. if (select && $sel[0] && $sel[0].type == 'select-one') {
  528. // deselect all other options
  529. $sel.find('option').selected(false);
  530. }
  531. this.selected = select;
  532. }
  533. });
  534. };
  535. })(jQuery);