jquery.fileupload.js 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867
  1. /*
  2. * jQuery File Upload Plugin 5.9
  3. * https://github.com/blueimp/jQuery-File-Upload
  4. *
  5. * Copyright 2010, Sebastian Tschan
  6. * https://blueimp.net
  7. *
  8. * Licensed under the MIT license:
  9. * http://www.opensource.org/licenses/MIT
  10. */
  11. /*jslint nomen: true, unparam: true, regexp: true */
  12. /*global define, window, document, Blob, FormData, location */
  13. (function (factory) {
  14. 'use strict';
  15. if (typeof define === 'function' && define.amd) {
  16. // Register as an anonymous AMD module:
  17. define([
  18. 'jquery',
  19. 'jquery.ui.widget'
  20. ], factory);
  21. } else {
  22. // Browser globals:
  23. factory(window.jQuery);
  24. }
  25. }(function ($) {
  26. 'use strict';
  27. // The FileReader API is not actually used, but works as feature detection,
  28. // as e.g. Safari supports XHR file uploads via the FormData API,
  29. // but not non-multipart XHR file uploads:
  30. $.support.xhrFileUpload = !!(window.XMLHttpRequestUpload && window.FileReader);
  31. $.support.xhrFormDataFileUpload = !!window.FormData;
  32. // The fileupload widget listens for change events on file input fields defined
  33. // via fileInput setting and paste or drop events of the given dropZone.
  34. // In addition to the default jQuery Widget methods, the fileupload widget
  35. // exposes the "add" and "send" methods, to add or directly send files using
  36. // the fileupload API.
  37. // By default, files added via file input selection, paste, drag & drop or
  38. // "add" method are uploaded immediately, but it is possible to override
  39. // the "add" callback option to queue file uploads.
  40. $.widget('blueimp.fileupload', {
  41. options: {
  42. // The namespace used for event handler binding on the dropZone and
  43. // fileInput collections.
  44. // If not set, the name of the widget ("fileupload") is used.
  45. namespace: undefined,
  46. // The drop target collection, by the default the complete document.
  47. // Set to null or an empty collection to disable drag & drop support:
  48. dropZone: $(document),
  49. // The file input field collection, that is listened for change events.
  50. // If undefined, it is set to the file input fields inside
  51. // of the widget element on plugin initialization.
  52. // Set to null or an empty collection to disable the change listener.
  53. fileInput: undefined,
  54. // By default, the file input field is replaced with a clone after
  55. // each input field change event. This is required for iframe transport
  56. // queues and allows change events to be fired for the same file
  57. // selection, but can be disabled by setting the following option to false:
  58. replaceFileInput: true,
  59. // The parameter name for the file form data (the request argument name).
  60. // If undefined or empty, the name property of the file input field is
  61. // used, or "files[]" if the file input name property is also empty:
  62. paramName: undefined,
  63. // By default, each file of a selection is uploaded using an individual
  64. // request for XHR type uploads. Set to false to upload file
  65. // selections in one request each:
  66. singleFileUploads: true,
  67. // To limit the number of files uploaded with one XHR request,
  68. // set the following option to an integer greater than 0:
  69. limitMultiFileUploads: undefined,
  70. // Set the following option to true to issue all file upload requests
  71. // in a sequential order:
  72. sequentialUploads: false,
  73. // To limit the number of concurrent uploads,
  74. // set the following option to an integer greater than 0:
  75. limitConcurrentUploads: undefined,
  76. // Set the following option to true to force iframe transport uploads:
  77. forceIframeTransport: false,
  78. // Set the following option to the location of a redirect url on the
  79. // origin server, for cross-domain iframe transport uploads:
  80. redirect: undefined,
  81. // The parameter name for the redirect url, sent as part of the form
  82. // data and set to 'redirect' if this option is empty:
  83. redirectParamName: undefined,
  84. // Set the following option to the location of a postMessage window,
  85. // to enable postMessage transport uploads:
  86. postMessage: undefined,
  87. // By default, XHR file uploads are sent as multipart/form-data.
  88. // The iframe transport is always using multipart/form-data.
  89. // Set to false to enable non-multipart XHR uploads:
  90. multipart: true,
  91. // To upload large files in smaller chunks, set the following option
  92. // to a preferred maximum chunk size. If set to 0, null or undefined,
  93. // or the browser does not support the required Blob API, files will
  94. // be uploaded as a whole.
  95. maxChunkSize: undefined,
  96. // When a non-multipart upload or a chunked multipart upload has been
  97. // aborted, this option can be used to resume the upload by setting
  98. // it to the size of the already uploaded bytes. This option is most
  99. // useful when modifying the options object inside of the "add" or
  100. // "send" callbacks, as the options are cloned for each file upload.
  101. uploadedBytes: undefined,
  102. // By default, failed (abort or error) file uploads are removed from the
  103. // global progress calculation. Set the following option to false to
  104. // prevent recalculating the global progress data:
  105. recalculateProgress: true,
  106. // Additional form data to be sent along with the file uploads can be set
  107. // using this option, which accepts an array of objects with name and
  108. // value properties, a function returning such an array, a FormData
  109. // object (for XHR file uploads), or a simple object.
  110. // The form of the first fileInput is given as parameter to the function:
  111. formData: function (form) {
  112. return form.serializeArray();
  113. },
  114. // The add callback is invoked as soon as files are added to the fileupload
  115. // widget (via file input selection, drag & drop, paste or add API call).
  116. // If the singleFileUploads option is enabled, this callback will be
  117. // called once for each file in the selection for XHR file uplaods, else
  118. // once for each file selection.
  119. // The upload starts when the submit method is invoked on the data parameter.
  120. // The data object contains a files property holding the added files
  121. // and allows to override plugin options as well as define ajax settings.
  122. // Listeners for this callback can also be bound the following way:
  123. // .bind('fileuploadadd', func);
  124. // data.submit() returns a Promise object and allows to attach additional
  125. // handlers using jQuery's Deferred callbacks:
  126. // data.submit().done(func).fail(func).always(func);
  127. add: function (e, data) {
  128. data.submit();
  129. },
  130. // Other callbacks:
  131. // Callback for the submit event of each file upload:
  132. // submit: function (e, data) {}, // .bind('fileuploadsubmit', func);
  133. // Callback for the start of each file upload request:
  134. // send: function (e, data) {}, // .bind('fileuploadsend', func);
  135. // Callback for successful uploads:
  136. // done: function (e, data) {}, // .bind('fileuploaddone', func);
  137. // Callback for failed (abort or error) uploads:
  138. // fail: function (e, data) {}, // .bind('fileuploadfail', func);
  139. // Callback for completed (success, abort or error) requests:
  140. // always: function (e, data) {}, // .bind('fileuploadalways', func);
  141. // Callback for upload progress events:
  142. // progress: function (e, data) {}, // .bind('fileuploadprogress', func);
  143. // Callback for global upload progress events:
  144. // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func);
  145. // Callback for uploads start, equivalent to the global ajaxStart event:
  146. // start: function (e) {}, // .bind('fileuploadstart', func);
  147. // Callback for uploads stop, equivalent to the global ajaxStop event:
  148. // stop: function (e) {}, // .bind('fileuploadstop', func);
  149. // Callback for change events of the fileInput collection:
  150. // change: function (e, data) {}, // .bind('fileuploadchange', func);
  151. // Callback for paste events to the dropZone collection:
  152. // paste: function (e, data) {}, // .bind('fileuploadpaste', func);
  153. // Callback for drop events of the dropZone collection:
  154. // drop: function (e, data) {}, // .bind('fileuploaddrop', func);
  155. // Callback for dragover events of the dropZone collection:
  156. // dragover: function (e) {}, // .bind('fileuploaddragover', func);
  157. // The plugin options are used as settings object for the ajax calls.
  158. // The following are jQuery ajax settings required for the file uploads:
  159. processData: false,
  160. contentType: false,
  161. cache: false
  162. },
  163. // A list of options that require a refresh after assigning a new value:
  164. _refreshOptionsList: [
  165. 'namespace',
  166. 'dropZone',
  167. 'fileInput',
  168. 'multipart',
  169. 'forceIframeTransport'
  170. ],
  171. _isXHRUpload: function (options) {
  172. return !options.forceIframeTransport &&
  173. ((!options.multipart && $.support.xhrFileUpload) ||
  174. $.support.xhrFormDataFileUpload);
  175. },
  176. _getFormData: function (options) {
  177. var formData;
  178. if (typeof options.formData === 'function') {
  179. return options.formData(options.form);
  180. } else if ($.isArray(options.formData)) {
  181. return options.formData;
  182. } else if (options.formData) {
  183. formData = [];
  184. $.each(options.formData, function (name, value) {
  185. formData.push({name: name, value: value});
  186. });
  187. return formData;
  188. }
  189. return [];
  190. },
  191. _getTotal: function (files) {
  192. var total = 0;
  193. $.each(files, function (index, file) {
  194. total += file.size || 1;
  195. });
  196. return total;
  197. },
  198. _onProgress: function (e, data) {
  199. if (e.lengthComputable) {
  200. var total = data.total || this._getTotal(data.files),
  201. loaded = parseInt(
  202. e.loaded / e.total * (data.chunkSize || total),
  203. 10
  204. ) + (data.uploadedBytes || 0);
  205. this._loaded += loaded - (data.loaded || data.uploadedBytes || 0);
  206. data.lengthComputable = true;
  207. data.loaded = loaded;
  208. data.total = total;
  209. // Trigger a custom progress event with a total data property set
  210. // to the file size(s) of the current upload and a loaded data
  211. // property calculated accordingly:
  212. this._trigger('progress', e, data);
  213. // Trigger a global progress event for all current file uploads,
  214. // including ajax calls queued for sequential file uploads:
  215. this._trigger('progressall', e, {
  216. lengthComputable: true,
  217. loaded: this._loaded,
  218. total: this._total
  219. });
  220. }
  221. },
  222. _initProgressListener: function (options) {
  223. var that = this,
  224. xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr();
  225. // Accesss to the native XHR object is required to add event listeners
  226. // for the upload progress event:
  227. if (xhr.upload) {
  228. $(xhr.upload).bind('progress', function (e) {
  229. var oe = e.originalEvent;
  230. // Make sure the progress event properties get copied over:
  231. e.lengthComputable = oe.lengthComputable;
  232. e.loaded = oe.loaded;
  233. e.total = oe.total;
  234. that._onProgress(e, options);
  235. });
  236. options.xhr = function () {
  237. return xhr;
  238. };
  239. }
  240. },
  241. _initXHRData: function (options) {
  242. var formData,
  243. file = options.files[0],
  244. // Ignore non-multipart setting if not supported:
  245. multipart = options.multipart || !$.support.xhrFileUpload;
  246. if (!multipart || options.blob) {
  247. // For non-multipart uploads and chunked uploads,
  248. // file meta data is not part of the request body,
  249. // so we transmit this data as part of the HTTP headers.
  250. // For cross domain requests, these headers must be allowed
  251. // via Access-Control-Allow-Headers or removed using
  252. // the beforeSend callback:
  253. options.headers = $.extend(options.headers, {
  254. 'X-File-Name': file.name,
  255. 'X-File-Type': file.type,
  256. 'X-File-Size': file.size
  257. });
  258. if (!options.blob) {
  259. // Non-chunked non-multipart upload:
  260. options.contentType = file.type;
  261. options.data = file;
  262. } else if (!multipart) {
  263. // Chunked non-multipart upload:
  264. options.contentType = 'application/octet-stream';
  265. options.data = options.blob;
  266. }
  267. }
  268. if (multipart && $.support.xhrFormDataFileUpload) {
  269. if (options.postMessage) {
  270. // window.postMessage does not allow sending FormData
  271. // objects, so we just add the File/Blob objects to
  272. // the formData array and let the postMessage window
  273. // create the FormData object out of this array:
  274. formData = this._getFormData(options);
  275. if (options.blob) {
  276. formData.push({
  277. name: options.paramName,
  278. value: options.blob
  279. });
  280. } else {
  281. $.each(options.files, function (index, file) {
  282. formData.push({
  283. name: options.paramName,
  284. value: file
  285. });
  286. });
  287. }
  288. } else {
  289. if (options.formData instanceof FormData) {
  290. formData = options.formData;
  291. } else {
  292. formData = new FormData();
  293. $.each(this._getFormData(options), function (index, field) {
  294. formData.append(field.name, field.value);
  295. });
  296. }
  297. if (options.blob) {
  298. formData.append(options.paramName, options.blob, file.name);
  299. } else {
  300. $.each(options.files, function (index, file) {
  301. // File objects are also Blob instances.
  302. // This check allows the tests to run with
  303. // dummy objects:
  304. if (file instanceof Blob) {
  305. formData.append(options.paramName, file, file.name);
  306. }
  307. });
  308. }
  309. }
  310. options.data = formData;
  311. }
  312. // Blob reference is not needed anymore, free memory:
  313. options.blob = null;
  314. },
  315. _initIframeSettings: function (options) {
  316. // Setting the dataType to iframe enables the iframe transport:
  317. options.dataType = 'iframe ' + (options.dataType || '');
  318. // The iframe transport accepts a serialized array as form data:
  319. options.formData = this._getFormData(options);
  320. // Add redirect url to form data on cross-domain uploads:
  321. if (options.redirect && $('<a></a>').prop('href', options.url)
  322. .prop('host') !== location.host) {
  323. options.formData.push({
  324. name: options.redirectParamName || 'redirect',
  325. value: options.redirect
  326. });
  327. }
  328. },
  329. _initDataSettings: function (options) {
  330. if (this._isXHRUpload(options)) {
  331. if (!this._chunkedUpload(options, true)) {
  332. if (!options.data) {
  333. this._initXHRData(options);
  334. }
  335. this._initProgressListener(options);
  336. }
  337. if (options.postMessage) {
  338. // Setting the dataType to postmessage enables the
  339. // postMessage transport:
  340. options.dataType = 'postmessage ' + (options.dataType || '');
  341. }
  342. } else {
  343. this._initIframeSettings(options, 'iframe');
  344. }
  345. },
  346. _initFormSettings: function (options) {
  347. // Retrieve missing options from the input field and the
  348. // associated form, if available:
  349. if (!options.form || !options.form.length) {
  350. options.form = $(options.fileInput.prop('form'));
  351. }
  352. if (!options.paramName) {
  353. options.paramName = options.fileInput.prop('name') ||
  354. 'files[]';
  355. }
  356. if (!options.url) {
  357. options.url = options.form.prop('action') || location.href;
  358. }
  359. // The HTTP request method must be "POST" or "PUT":
  360. options.type = (options.type || options.form.prop('method') || '')
  361. .toUpperCase();
  362. if (options.type !== 'POST' && options.type !== 'PUT') {
  363. options.type = 'POST';
  364. }
  365. },
  366. _getAJAXSettings: function (data) {
  367. var options = $.extend({}, this.options, data);
  368. this._initFormSettings(options);
  369. this._initDataSettings(options);
  370. return options;
  371. },
  372. // Maps jqXHR callbacks to the equivalent
  373. // methods of the given Promise object:
  374. _enhancePromise: function (promise) {
  375. promise.success = promise.done;
  376. promise.error = promise.fail;
  377. promise.complete = promise.always;
  378. return promise;
  379. },
  380. // Creates and returns a Promise object enhanced with
  381. // the jqXHR methods abort, success, error and complete:
  382. _getXHRPromise: function (resolveOrReject, context, args) {
  383. var dfd = $.Deferred(),
  384. promise = dfd.promise();
  385. context = context || this.options.context || promise;
  386. if (resolveOrReject === true) {
  387. dfd.resolveWith(context, args);
  388. } else if (resolveOrReject === false) {
  389. dfd.rejectWith(context, args);
  390. }
  391. promise.abort = dfd.promise;
  392. return this._enhancePromise(promise);
  393. },
  394. // Uploads a file in multiple, sequential requests
  395. // by splitting the file up in multiple blob chunks.
  396. // If the second parameter is true, only tests if the file
  397. // should be uploaded in chunks, but does not invoke any
  398. // upload requests:
  399. _chunkedUpload: function (options, testOnly) {
  400. var that = this,
  401. file = options.files[0],
  402. fs = file.size,
  403. ub = options.uploadedBytes = options.uploadedBytes || 0,
  404. mcs = options.maxChunkSize || fs,
  405. // Use the Blob methods with the slice implementation
  406. // according to the W3C Blob API specification:
  407. slice = file.webkitSlice || file.mozSlice || file.slice,
  408. upload,
  409. n,
  410. jqXHR,
  411. pipe;
  412. if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) ||
  413. options.data) {
  414. return false;
  415. }
  416. if (testOnly) {
  417. return true;
  418. }
  419. if (ub >= fs) {
  420. file.error = 'uploadedBytes';
  421. return this._getXHRPromise(
  422. false,
  423. options.context,
  424. [null, 'error', file.error]
  425. );
  426. }
  427. // n is the number of blobs to upload,
  428. // calculated via filesize, uploaded bytes and max chunk size:
  429. n = Math.ceil((fs - ub) / mcs);
  430. // The chunk upload method accepting the chunk number as parameter:
  431. upload = function (i) {
  432. if (!i) {
  433. return that._getXHRPromise(true, options.context);
  434. }
  435. // Upload the blobs in sequential order:
  436. return upload(i -= 1).pipe(function () {
  437. // Clone the options object for each chunk upload:
  438. var o = $.extend({}, options);
  439. o.blob = slice.call(
  440. file,
  441. ub + i * mcs,
  442. ub + (i + 1) * mcs
  443. );
  444. // Store the current chunk size, as the blob itself
  445. // will be dereferenced after data processing:
  446. o.chunkSize = o.blob.size;
  447. // Process the upload data (the blob and potential form data):
  448. that._initXHRData(o);
  449. // Add progress listeners for this chunk upload:
  450. that._initProgressListener(o);
  451. jqXHR = ($.ajax(o) || that._getXHRPromise(false, o.context))
  452. .done(function () {
  453. // Create a progress event if upload is done and
  454. // no progress event has been invoked for this chunk:
  455. if (!o.loaded) {
  456. that._onProgress($.Event('progress', {
  457. lengthComputable: true,
  458. loaded: o.chunkSize,
  459. total: o.chunkSize
  460. }), o);
  461. }
  462. options.uploadedBytes = o.uploadedBytes +=
  463. o.chunkSize;
  464. });
  465. return jqXHR;
  466. });
  467. };
  468. // Return the piped Promise object, enhanced with an abort method,
  469. // which is delegated to the jqXHR object of the current upload,
  470. // and jqXHR callbacks mapped to the equivalent Promise methods:
  471. pipe = upload(n);
  472. pipe.abort = function () {
  473. return jqXHR.abort();
  474. };
  475. return this._enhancePromise(pipe);
  476. },
  477. _beforeSend: function (e, data) {
  478. if (this._active === 0) {
  479. // the start callback is triggered when an upload starts
  480. // and no other uploads are currently running,
  481. // equivalent to the global ajaxStart event:
  482. this._trigger('start');
  483. }
  484. this._active += 1;
  485. // Initialize the global progress values:
  486. this._loaded += data.uploadedBytes || 0;
  487. this._total += this._getTotal(data.files);
  488. },
  489. _onDone: function (result, textStatus, jqXHR, options) {
  490. if (!this._isXHRUpload(options)) {
  491. // Create a progress event for each iframe load:
  492. this._onProgress($.Event('progress', {
  493. lengthComputable: true,
  494. loaded: 1,
  495. total: 1
  496. }), options);
  497. }
  498. options.result = result;
  499. options.textStatus = textStatus;
  500. options.jqXHR = jqXHR;
  501. this._trigger('done', null, options);
  502. },
  503. _onFail: function (jqXHR, textStatus, errorThrown, options) {
  504. options.jqXHR = jqXHR;
  505. options.textStatus = textStatus;
  506. options.errorThrown = errorThrown;
  507. this._trigger('fail', null, options);
  508. if (options.recalculateProgress) {
  509. // Remove the failed (error or abort) file upload from
  510. // the global progress calculation:
  511. this._loaded -= options.loaded || options.uploadedBytes || 0;
  512. this._total -= options.total || this._getTotal(options.files);
  513. }
  514. },
  515. _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) {
  516. this._active -= 1;
  517. options.textStatus = textStatus;
  518. if (jqXHRorError && jqXHRorError.always) {
  519. options.jqXHR = jqXHRorError;
  520. options.result = jqXHRorResult;
  521. } else {
  522. options.jqXHR = jqXHRorResult;
  523. options.errorThrown = jqXHRorError;
  524. }
  525. this._trigger('always', null, options);
  526. if (this._active === 0) {
  527. // The stop callback is triggered when all uploads have
  528. // been completed, equivalent to the global ajaxStop event:
  529. this._trigger('stop');
  530. // Reset the global progress values:
  531. this._loaded = this._total = 0;
  532. }
  533. },
  534. _onSend: function (e, data) {
  535. var that = this,
  536. jqXHR,
  537. slot,
  538. pipe,
  539. options = that._getAJAXSettings(data),
  540. send = function (resolve, args) {
  541. that._sending += 1;
  542. jqXHR = jqXHR || (
  543. (resolve !== false &&
  544. that._trigger('send', e, options) !== false &&
  545. (that._chunkedUpload(options) || $.ajax(options))) ||
  546. that._getXHRPromise(false, options.context, args)
  547. ).done(function (result, textStatus, jqXHR) {
  548. that._onDone(result, textStatus, jqXHR, options);
  549. }).fail(function (jqXHR, textStatus, errorThrown) {
  550. that._onFail(jqXHR, textStatus, errorThrown, options);
  551. }).always(function (jqXHRorResult, textStatus, jqXHRorError) {
  552. that._sending -= 1;
  553. that._onAlways(
  554. jqXHRorResult,
  555. textStatus,
  556. jqXHRorError,
  557. options
  558. );
  559. if (options.limitConcurrentUploads &&
  560. options.limitConcurrentUploads > that._sending) {
  561. // Start the next queued upload,
  562. // that has not been aborted:
  563. var nextSlot = that._slots.shift();
  564. while (nextSlot) {
  565. if (!nextSlot.isRejected()) {
  566. nextSlot.resolve();
  567. break;
  568. }
  569. nextSlot = that._slots.shift();
  570. }
  571. }
  572. });
  573. return jqXHR;
  574. };
  575. this._beforeSend(e, options);
  576. if (this.options.sequentialUploads ||
  577. (this.options.limitConcurrentUploads &&
  578. this.options.limitConcurrentUploads <= this._sending)) {
  579. if (this.options.limitConcurrentUploads > 1) {
  580. slot = $.Deferred();
  581. this._slots.push(slot);
  582. pipe = slot.pipe(send);
  583. } else {
  584. pipe = (this._sequence = this._sequence.pipe(send, send));
  585. }
  586. // Return the piped Promise object, enhanced with an abort method,
  587. // which is delegated to the jqXHR object of the current upload,
  588. // and jqXHR callbacks mapped to the equivalent Promise methods:
  589. pipe.abort = function () {
  590. var args = [undefined, 'abort', 'abort'];
  591. if (!jqXHR) {
  592. if (slot) {
  593. slot.rejectWith(args);
  594. }
  595. return send(false, args);
  596. }
  597. return jqXHR.abort();
  598. };
  599. return this._enhancePromise(pipe);
  600. }
  601. return send();
  602. },
  603. _onAdd: function (e, data) {
  604. var that = this,
  605. result = true,
  606. options = $.extend({}, this.options, data),
  607. limit = options.limitMultiFileUploads,
  608. fileSet,
  609. i;
  610. if (!(options.singleFileUploads || limit) ||
  611. !this._isXHRUpload(options)) {
  612. fileSet = [data.files];
  613. } else if (!options.singleFileUploads && limit) {
  614. fileSet = [];
  615. for (i = 0; i < data.files.length; i += limit) {
  616. fileSet.push(data.files.slice(i, i + limit));
  617. }
  618. }
  619. data.originalFiles = data.files;
  620. $.each(fileSet || data.files, function (index, element) {
  621. var files = fileSet ? element : [element],
  622. newData = $.extend({}, data, {files: files});
  623. newData.submit = function () {
  624. newData.jqXHR = this.jqXHR =
  625. (that._trigger('submit', e, this) !== false) &&
  626. that._onSend(e, this);
  627. return this.jqXHR;
  628. };
  629. return (result = that._trigger('add', e, newData));
  630. });
  631. return result;
  632. },
  633. // File Normalization for Gecko 1.9.1 (Firefox 3.5) support:
  634. _normalizeFile: function (index, file) {
  635. if (file.name === undefined && file.size === undefined) {
  636. file.name = file.fileName;
  637. file.size = file.fileSize;
  638. }
  639. },
  640. _replaceFileInput: function (input) {
  641. var inputClone = input.clone(true);
  642. $('<form></form>').append(inputClone)[0].reset();
  643. // Detaching allows to insert the fileInput on another form
  644. // without loosing the file input value:
  645. input.after(inputClone).detach();
  646. // Avoid memory leaks with the detached file input:
  647. $.cleanData(input.unbind('remove'));
  648. // Replace the original file input element in the fileInput
  649. // collection with the clone, which has been copied including
  650. // event handlers:
  651. this.options.fileInput = this.options.fileInput.map(function (i, el) {
  652. if (el === input[0]) {
  653. return inputClone[0];
  654. }
  655. return el;
  656. });
  657. // If the widget has been initialized on the file input itself,
  658. // override this.element with the file input clone:
  659. if (input[0] === this.element[0]) {
  660. this.element = inputClone;
  661. }
  662. },
  663. _onChange: function (e) {
  664. var that = e.data.fileupload,
  665. data = {
  666. files: $.each($.makeArray(e.target.files), that._normalizeFile),
  667. fileInput: $(e.target),
  668. form: $(e.target.form)
  669. };
  670. if (!data.files.length) {
  671. // If the files property is not available, the browser does not
  672. // support the File API and we add a pseudo File object with
  673. // the input value as name with path information removed:
  674. data.files = [{name: e.target.value.replace(/^.*\\/, '')}];
  675. }
  676. if (that.options.replaceFileInput) {
  677. that._replaceFileInput(data.fileInput);
  678. }
  679. if (that._trigger('change', e, data) === false ||
  680. that._onAdd(e, data) === false) {
  681. return false;
  682. }
  683. },
  684. _onPaste: function (e) {
  685. var that = e.data.fileupload,
  686. cbd = e.originalEvent.clipboardData,
  687. items = (cbd && cbd.items) || [],
  688. data = {files: []};
  689. $.each(items, function (index, item) {
  690. var file = item.getAsFile && item.getAsFile();
  691. if (file) {
  692. data.files.push(file);
  693. }
  694. });
  695. if (that._trigger('paste', e, data) === false ||
  696. that._onAdd(e, data) === false) {
  697. return false;
  698. }
  699. },
  700. _onDrop: function (e) {
  701. var that = e.data.fileupload,
  702. dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer,
  703. data = {
  704. files: $.each(
  705. $.makeArray(dataTransfer && dataTransfer.files),
  706. that._normalizeFile
  707. )
  708. };
  709. if (that._trigger('drop', e, data) === false ||
  710. that._onAdd(e, data) === false) {
  711. return false;
  712. }
  713. e.preventDefault();
  714. },
  715. _onDragOver: function (e) {
  716. var that = e.data.fileupload,
  717. dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer;
  718. if (that._trigger('dragover', e) === false) {
  719. return false;
  720. }
  721. if (dataTransfer) {
  722. dataTransfer.dropEffect = dataTransfer.effectAllowed = 'copy';
  723. }
  724. e.preventDefault();
  725. },
  726. _initEventHandlers: function () {
  727. var ns = this.options.namespace;
  728. if (this._isXHRUpload(this.options)) {
  729. this.options.dropZone
  730. .bind('dragover.' + ns, {fileupload: this}, this._onDragOver)
  731. .bind('drop.' + ns, {fileupload: this}, this._onDrop)
  732. .bind('paste.' + ns, {fileupload: this}, this._onPaste);
  733. }
  734. this.options.fileInput
  735. .bind('change.' + ns, {fileupload: this}, this._onChange);
  736. },
  737. _destroyEventHandlers: function () {
  738. var ns = this.options.namespace;
  739. this.options.dropZone
  740. .unbind('dragover.' + ns, this._onDragOver)
  741. .unbind('drop.' + ns, this._onDrop)
  742. .unbind('paste.' + ns, this._onPaste);
  743. this.options.fileInput
  744. .unbind('change.' + ns, this._onChange);
  745. },
  746. _setOption: function (key, value) {
  747. var refresh = $.inArray(key, this._refreshOptionsList) !== -1;
  748. if (refresh) {
  749. this._destroyEventHandlers();
  750. }
  751. $.Widget.prototype._setOption.call(this, key, value);
  752. if (refresh) {
  753. this._initSpecialOptions();
  754. this._initEventHandlers();
  755. }
  756. },
  757. _initSpecialOptions: function () {
  758. var options = this.options;
  759. if (options.fileInput === undefined) {
  760. options.fileInput = this.element.is('input:file') ?
  761. this.element : this.element.find('input:file');
  762. } else if (!(options.fileInput instanceof $)) {
  763. options.fileInput = $(options.fileInput);
  764. }
  765. if (!(options.dropZone instanceof $)) {
  766. options.dropZone = $(options.dropZone);
  767. }
  768. },
  769. _create: function () {
  770. var options = this.options,
  771. dataOpts = $.extend({}, this.element.data());
  772. dataOpts[this.widgetName] = undefined;
  773. $.extend(options, dataOpts);
  774. options.namespace = options.namespace || this.widgetName;
  775. this._initSpecialOptions();
  776. this._slots = [];
  777. this._sequence = this._getXHRPromise(true);
  778. this._sending = this._active = this._loaded = this._total = 0;
  779. this._initEventHandlers();
  780. },
  781. destroy: function () {
  782. this._destroyEventHandlers();
  783. $.Widget.prototype.destroy.call(this);
  784. },
  785. enable: function () {
  786. $.Widget.prototype.enable.call(this);
  787. this._initEventHandlers();
  788. },
  789. disable: function () {
  790. this._destroyEventHandlers();
  791. $.Widget.prototype.disable.call(this);
  792. },
  793. // This method is exposed to the widget API and allows adding files
  794. // using the fileupload API. The data parameter accepts an object which
  795. // must have a files property and can contain additional options:
  796. // .fileupload('add', {files: filesList});
  797. add: function (data) {
  798. if (!data || this.options.disabled) {
  799. return;
  800. }
  801. data.files = $.each($.makeArray(data.files), this._normalizeFile);
  802. this._onAdd(null, data);
  803. },
  804. // This method is exposed to the widget API and allows sending files
  805. // using the fileupload API. The data parameter accepts an object which
  806. // must have a files property and can contain additional options:
  807. // .fileupload('send', {files: filesList});
  808. // The method returns a Promise object for the file upload call.
  809. send: function (data) {
  810. if (data && !this.options.disabled) {
  811. data.files = $.each($.makeArray(data.files), this._normalizeFile);
  812. if (data.files.length) {
  813. return this._onSend(null, data);
  814. }
  815. }
  816. return this._getXHRPromise(false, data && data.context);
  817. }
  818. });
  819. }));