jquery.iframe-transport.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. // This [jQuery](https://jquery.com/) plugin implements an `<iframe>`
  2. // [transport](https://api.jquery.com/jQuery.ajax/#extending-ajax) so that
  3. // `$.ajax()` calls support the uploading of files using standard HTML file
  4. // input fields. This is done by switching the exchange from `XMLHttpRequest`
  5. // to a hidden `iframe` element containing a form that is submitted.
  6. // The [source for the plugin](https://github.com/cmlenz/jquery-iframe-transport)
  7. // is available on [Github](https://github.com/) and licensed under the [MIT
  8. // license](https://github.com/cmlenz/jquery-iframe-transport/blob/master/LICENSE).
  9. // ## Usage
  10. // To use this plugin, you simply add an `iframe` option with the value `true`
  11. // to the Ajax settings an `$.ajax()` call, and specify the file fields to
  12. // include in the submssion using the `files` option, which can be a selector,
  13. // jQuery object, or a list of DOM elements containing one or more
  14. // `<input type="file">` elements:
  15. // $("#myform").submit(function() {
  16. // $.ajax(this.action, {
  17. // files: $(":file", this),
  18. // iframe: true
  19. // }).complete(function(data) {
  20. // console.log(data);
  21. // });
  22. // });
  23. // The plugin will construct hidden `<iframe>` and `<form>` elements, add the
  24. // file field(s) to that form, submit the form, and process the response.
  25. // If you want to include other form fields in the form submission, include
  26. // them in the `data` option, and set the `processData` option to `false`:
  27. // $("#myform").submit(function() {
  28. // $.ajax(this.action, {
  29. // data: $(":text", this).serializeArray(),
  30. // files: $(":file", this),
  31. // iframe: true,
  32. // processData: false
  33. // }).complete(function(data) {
  34. // console.log(data);
  35. // });
  36. // });
  37. // ### Response Data Types
  38. // As the transport does not have access to the HTTP headers of the server
  39. // response, it is not as simple to make use of the automatic content type
  40. // detection provided by jQuery as with regular XHR. If you can't set the
  41. // expected response data type (for example because it may vary depending on
  42. // the outcome of processing by the server), you will need to employ a
  43. // workaround on the server side: Send back an HTML document containing just a
  44. // `<textarea>` element with a `data-type` attribute that specifies the MIME
  45. // type, and put the actual payload in the textarea:
  46. // <textarea data-type="application/json">
  47. // {"ok": true, "message": "Thanks so much"}
  48. // </textarea>
  49. // The iframe transport plugin will detect this and pass the value of the
  50. // `data-type` attribute on to jQuery as if it was the "Content-Type" response
  51. // header, thereby enabling the same kind of conversions that jQuery applies
  52. // to regular responses. For the example above you should get a Javascript
  53. // object as the `data` parameter of the `complete` callback, with the
  54. // properties `ok: true` and `message: "Thanks so much"`.
  55. // ### Handling Server Errors
  56. // Another problem with using an `iframe` for file uploads is that it is
  57. // impossible for the javascript code to determine the HTTP status code of the
  58. // servers response. Effectively, all of the calls you make will look like they
  59. // are getting successful responses, and thus invoke the `done()` or
  60. // `complete()` callbacks. You can only communicate problems using the content
  61. // of the response payload. For example, consider using a JSON response such as
  62. // the following to indicate a problem with an uploaded file:
  63. // <textarea data-type="application/json">
  64. // {"ok": false, "message": "Please only upload reasonably sized files."}
  65. // </textarea>
  66. // ### Compatibility
  67. // This plugin has primarily been tested on Safari 5 (or later), Firefox 4 (or
  68. // later), and Internet Explorer (all the way back to version 6). While I
  69. // haven't found any issues with it so far, I'm fairly sure it still doesn't
  70. // work around all the quirks in all different browsers. But the code is still
  71. // pretty simple overall, so you should be able to fix it and contribute a
  72. // patch :)
  73. // ## Annotated Source
  74. (function($, undefined) {
  75. "use strict";
  76. // Register a prefilter that checks whether the `iframe` option is set, and
  77. // switches to the "iframe" data type if it is `true`.
  78. $.ajaxPrefilter(function(options, origOptions, jqXHR) {
  79. if (options.iframe) {
  80. options.originalURL = options.url;
  81. return "iframe";
  82. }
  83. });
  84. // Register a transport for the "iframe" data type. It will only activate
  85. // when the "files" option has been set to a non-empty list of enabled file
  86. // inputs.
  87. $.ajaxTransport("iframe", function(options, origOptions, jqXHR) {
  88. var form = null,
  89. iframe = null,
  90. name = "iframe-" + $.now(),
  91. files = $(options.files).filter(":file:enabled"),
  92. markers = null,
  93. accepts = null;
  94. // This function gets called after a successful submission or an abortion
  95. // and should revert all changes made to the page to enable the
  96. // submission via this transport.
  97. function cleanUp() {
  98. files.each(function(i, file) {
  99. var $file = $(file);
  100. $file.data("clone").replaceWith($file);
  101. });
  102. form.remove();
  103. iframe.one("load", function() { iframe.remove(); });
  104. iframe.attr("src", "javascript:false;");
  105. }
  106. // Remove "iframe" from the data types list so that further processing is
  107. // based on the content type returned by the server, without attempting an
  108. // (unsupported) conversion from "iframe" to the actual type.
  109. options.dataTypes.shift();
  110. // Use the data from the original AJAX options, as it doesn't seem to be
  111. // copied over since jQuery 1.7.
  112. // See https://github.com/cmlenz/jquery-iframe-transport/issues/6
  113. options.data = origOptions.data;
  114. if (files.length) {
  115. form = $("<form enctype='multipart/form-data' method='post'></form>").
  116. hide().attr({action: options.originalURL, target: name});
  117. // If there is any additional data specified via the `data` option,
  118. // we add it as hidden fields to the form. This (currently) requires
  119. // the `processData` option to be set to false so that the data doesn't
  120. // get serialized to a string.
  121. if (typeof(options.data) === "string" && options.data.length > 0) {
  122. $.error("data must not be serialized");
  123. }
  124. $.each(options.data || {}, function(name, value) {
  125. if ($.isPlainObject(value)) {
  126. name = value.name;
  127. value = value.value;
  128. }
  129. $("<input type='hidden' />").attr({name: name, value: value}).
  130. appendTo(form);
  131. });
  132. // Add a hidden `X-Requested-With` field with the value `IFrame` to the
  133. // field, to help server-side code to determine that the upload happened
  134. // through this transport.
  135. $("<input type='hidden' value='IFrame' name='X-Requested-With' />").
  136. appendTo(form);
  137. // Borrowed straight from the JQuery source.
  138. // Provides a way of specifying the accepted data type similar to the
  139. // HTTP "Accept" header
  140. if (options.dataTypes[0] && options.accepts[options.dataTypes[0]]) {
  141. accepts = options.accepts[options.dataTypes[0]] +
  142. (options.dataTypes[0] !== "*" ? ", */*; q=0.01" : "");
  143. } else {
  144. accepts = options.accepts["*"];
  145. }
  146. $("<input type='hidden' name='X-HTTP-Accept'>").
  147. attr("value", accepts).appendTo(form);
  148. // Move the file fields into the hidden form, but first remember their
  149. // original locations in the document by replacing them with disabled
  150. // clones. This should also avoid introducing unwanted changes to the
  151. // page layout during submission.
  152. markers = files.after(function(idx) {
  153. var $this = $(this),
  154. $clone = $this.clone().prop("disabled", true);
  155. $this.data("clone", $clone);
  156. return $clone;
  157. }).next();
  158. files.appendTo(form);
  159. return {
  160. // The `send` function is called by jQuery when the request should be
  161. // sent.
  162. send: function(headers, completeCallback) {
  163. iframe = $("<iframe src='javascript:false;' name='" + name +
  164. "' id='" + name + "' style='display:none'></iframe>");
  165. // The first load event gets fired after the iframe has been injected
  166. // into the DOM, and is used to prepare the actual submission.
  167. iframe.one("load", function() {
  168. // The second load event gets fired when the response to the form
  169. // submission is received. The implementation detects whether the
  170. // actual payload is embedded in a `<textarea>` element, and
  171. // prepares the required conversions to be made in that case.
  172. iframe.one("load", function() {
  173. var doc = this.contentWindow ? this.contentWindow.document :
  174. (this.contentDocument ? this.contentDocument : this.document),
  175. root = doc.documentElement ? doc.documentElement : doc.body,
  176. textarea = root.getElementsByTagName("textarea")[0],
  177. type = textarea && textarea.getAttribute("data-type") || null,
  178. status = textarea && textarea.getAttribute("data-status") || 200,
  179. statusText = textarea && textarea.getAttribute("data-statusText") || "OK",
  180. content = {
  181. html: root.innerHTML,
  182. text: type ?
  183. textarea.value :
  184. root ? (root.textContent || root.innerText) : null
  185. };
  186. cleanUp();
  187. completeCallback(status, statusText, content, type ?
  188. ("Content-Type: " + type) :
  189. null);
  190. });
  191. // Now that the load handler has been set up, submit the form.
  192. form[0].submit();
  193. });
  194. // After everything has been set up correctly, the form and iframe
  195. // get injected into the DOM so that the submission can be
  196. // initiated.
  197. $("body").append(form, iframe);
  198. },
  199. // The `abort` function is called by jQuery when the request should be
  200. // aborted.
  201. abort: function() {
  202. if (iframe !== null) {
  203. iframe.unbind("load").attr("src", "javascript:false;");
  204. cleanUp();
  205. }
  206. }
  207. };
  208. }
  209. });
  210. })(jQuery);