You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1466 lines
62 KiB

  1. /*
  2. * jQuery File Upload Plugin 5.42.3
  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. /* jshint nomen:false */
  12. /* global define, require, window, document, location, Blob, FormData */
  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 if (typeof exports === 'object') {
  22. // Node/CommonJS:
  23. factory(
  24. require('jquery'),
  25. require('./vendor/jquery.ui.widget')
  26. );
  27. } else {
  28. // Browser globals:
  29. factory(window.jQuery);
  30. }
  31. }(function ($) {
  32. 'use strict';
  33. // Detect file input support, based on
  34. // http://viljamis.com/blog/2012/file-upload-support-on-mobile/
  35. $.support.fileInput = !(new RegExp(
  36. // Handle devices which give false positives for the feature detection:
  37. '(Android (1\\.[0156]|2\\.[01]))' +
  38. '|(Windows Phone (OS 7|8\\.0))|(XBLWP)|(ZuneWP)|(WPDesktop)' +
  39. '|(w(eb)?OSBrowser)|(webOS)' +
  40. '|(Kindle/(1\\.0|2\\.[05]|3\\.0))'
  41. ).test(window.navigator.userAgent) ||
  42. // Feature detection for all other devices:
  43. $('<input type="file">').prop('disabled'));
  44. // The FileReader API is not actually used, but works as feature detection,
  45. // as some Safari versions (5?) support XHR file uploads via the FormData API,
  46. // but not non-multipart XHR file uploads.
  47. // window.XMLHttpRequestUpload is not available on IE10, so we check for
  48. // window.ProgressEvent instead to detect XHR2 file upload capability:
  49. $.support.xhrFileUpload = !!(window.ProgressEvent && window.FileReader);
  50. $.support.xhrFormDataFileUpload = !!window.FormData;
  51. // Detect support for Blob slicing (required for chunked uploads):
  52. $.support.blobSlice = window.Blob && (Blob.prototype.slice ||
  53. Blob.prototype.webkitSlice || Blob.prototype.mozSlice);
  54. // Helper function to create drag handlers for dragover/dragenter/dragleave:
  55. function getDragHandler(type) {
  56. var isDragOver = type === 'dragover';
  57. return function (e) {
  58. e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer;
  59. var dataTransfer = e.dataTransfer;
  60. if (dataTransfer && $.inArray('Files', dataTransfer.types) !== -1 &&
  61. this._trigger(
  62. type,
  63. $.Event(type, {delegatedEvent: e})
  64. ) !== false) {
  65. e.preventDefault();
  66. if (isDragOver) {
  67. dataTransfer.dropEffect = 'copy';
  68. }
  69. }
  70. };
  71. }
  72. // The fileupload widget listens for change events on file input fields defined
  73. // via fileInput setting and paste or drop events of the given dropZone.
  74. // In addition to the default jQuery Widget methods, the fileupload widget
  75. // exposes the "add" and "send" methods, to add or directly send files using
  76. // the fileupload API.
  77. // By default, files added via file input selection, paste, drag & drop or
  78. // "add" method are uploaded immediately, but it is possible to override
  79. // the "add" callback option to queue file uploads.
  80. $.widget('blueimp.fileupload', {
  81. options: {
  82. // The drop target element(s), by the default the complete document.
  83. // Set to null to disable drag & drop support:
  84. dropZone: $(document),
  85. // The paste target element(s), by the default undefined.
  86. // Set to a DOM node or jQuery object to enable file pasting:
  87. pasteZone: undefined,
  88. // The file input field(s), that are listened to for change events.
  89. // If undefined, it is set to the file input fields inside
  90. // of the widget element on plugin initialization.
  91. // Set to null to disable the change listener.
  92. fileInput: undefined,
  93. // By default, the file input field is replaced with a clone after
  94. // each input field change event. This is required for iframe transport
  95. // queues and allows change events to be fired for the same file
  96. // selection, but can be disabled by setting the following option to false:
  97. replaceFileInput: true,
  98. // The parameter name for the file form data (the request argument name).
  99. // If undefined or empty, the name property of the file input field is
  100. // used, or "files[]" if the file input name property is also empty,
  101. // can be a string or an array of strings:
  102. paramName: undefined,
  103. // By default, each file of a selection is uploaded using an individual
  104. // request for XHR type uploads. Set to false to upload file
  105. // selections in one request each:
  106. singleFileUploads: true,
  107. // To limit the number of files uploaded with one XHR request,
  108. // set the following option to an integer greater than 0:
  109. limitMultiFileUploads: undefined,
  110. // The following option limits the number of files uploaded with one
  111. // XHR request to keep the request size under or equal to the defined
  112. // limit in bytes:
  113. limitMultiFileUploadSize: undefined,
  114. // Multipart file uploads add a number of bytes to each uploaded file,
  115. // therefore the following option adds an overhead for each file used
  116. // in the limitMultiFileUploadSize configuration:
  117. limitMultiFileUploadSizeOverhead: 512,
  118. // Set the following option to true to issue all file upload requests
  119. // in a sequential order:
  120. sequentialUploads: false,
  121. // To limit the number of concurrent uploads,
  122. // set the following option to an integer greater than 0:
  123. limitConcurrentUploads: undefined,
  124. // Set the following option to true to force iframe transport uploads:
  125. forceIframeTransport: false,
  126. // Set the following option to the location of a redirect url on the
  127. // origin server, for cross-domain iframe transport uploads:
  128. redirect: undefined,
  129. // The parameter name for the redirect url, sent as part of the form
  130. // data and set to 'redirect' if this option is empty:
  131. redirectParamName: undefined,
  132. // Set the following option to the location of a postMessage window,
  133. // to enable postMessage transport uploads:
  134. postMessage: undefined,
  135. // By default, XHR file uploads are sent as multipart/form-data.
  136. // The iframe transport is always using multipart/form-data.
  137. // Set to false to enable non-multipart XHR uploads:
  138. multipart: true,
  139. // To upload large files in smaller chunks, set the following option
  140. // to a preferred maximum chunk size. If set to 0, null or undefined,
  141. // or the browser does not support the required Blob API, files will
  142. // be uploaded as a whole.
  143. maxChunkSize: undefined,
  144. // When a non-multipart upload or a chunked multipart upload has been
  145. // aborted, this option can be used to resume the upload by setting
  146. // it to the size of the already uploaded bytes. This option is most
  147. // useful when modifying the options object inside of the "add" or
  148. // "send" callbacks, as the options are cloned for each file upload.
  149. uploadedBytes: undefined,
  150. // By default, failed (abort or error) file uploads are removed from the
  151. // global progress calculation. Set the following option to false to
  152. // prevent recalculating the global progress data:
  153. recalculateProgress: true,
  154. // Interval in milliseconds to calculate and trigger progress events:
  155. progressInterval: 100,
  156. // Interval in milliseconds to calculate progress bitrate:
  157. bitrateInterval: 500,
  158. // By default, uploads are started automatically when adding files:
  159. autoUpload: true,
  160. // Error and info messages:
  161. messages: {
  162. uploadedBytes: 'Uploaded bytes exceed file size'
  163. },
  164. // Translation function, gets the message key to be translated
  165. // and an object with context specific data as arguments:
  166. i18n: function (message, context) {
  167. message = this.messages[message] || message.toString();
  168. if (context) {
  169. $.each(context, function (key, value) {
  170. message = message.replace('{' + key + '}', value);
  171. });
  172. }
  173. return message;
  174. },
  175. // Additional form data to be sent along with the file uploads can be set
  176. // using this option, which accepts an array of objects with name and
  177. // value properties, a function returning such an array, a FormData
  178. // object (for XHR file uploads), or a simple object.
  179. // The form of the first fileInput is given as parameter to the function:
  180. formData: function (form) {
  181. return form.serializeArray();
  182. },
  183. // The add callback is invoked as soon as files are added to the fileupload
  184. // widget (via file input selection, drag & drop, paste or add API call).
  185. // If the singleFileUploads option is enabled, this callback will be
  186. // called once for each file in the selection for XHR file uploads, else
  187. // once for each file selection.
  188. //
  189. // The upload starts when the submit method is invoked on the data parameter.
  190. // The data object contains a files property holding the added files
  191. // and allows you to override plugin options as well as define ajax settings.
  192. //
  193. // Listeners for this callback can also be bound the following way:
  194. // .bind('fileuploadadd', func);
  195. //
  196. // data.submit() returns a Promise object and allows to attach additional
  197. // handlers using jQuery's Deferred callbacks:
  198. // data.submit().done(func).fail(func).always(func);
  199. add: function (e, data) {
  200. if (e.isDefaultPrevented()) {
  201. return false;
  202. }
  203. if (data.autoUpload || (data.autoUpload !== false &&
  204. $(this).fileupload('option', 'autoUpload'))) {
  205. data.process().done(function () {
  206. data.submit();
  207. });
  208. }
  209. },
  210. // Other callbacks:
  211. // Callback for the submit event of each file upload:
  212. // submit: function (e, data) {}, // .bind('fileuploadsubmit', func);
  213. // Callback for the start of each file upload request:
  214. // send: function (e, data) {}, // .bind('fileuploadsend', func);
  215. // Callback for successful uploads:
  216. // done: function (e, data) {}, // .bind('fileuploaddone', func);
  217. // Callback for failed (abort or error) uploads:
  218. // fail: function (e, data) {}, // .bind('fileuploadfail', func);
  219. // Callback for completed (success, abort or error) requests:
  220. // always: function (e, data) {}, // .bind('fileuploadalways', func);
  221. // Callback for upload progress events:
  222. // progress: function (e, data) {}, // .bind('fileuploadprogress', func);
  223. // Callback for global upload progress events:
  224. // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func);
  225. // Callback for uploads start, equivalent to the global ajaxStart event:
  226. // start: function (e) {}, // .bind('fileuploadstart', func);
  227. // Callback for uploads stop, equivalent to the global ajaxStop event:
  228. // stop: function (e) {}, // .bind('fileuploadstop', func);
  229. // Callback for change events of the fileInput(s):
  230. // change: function (e, data) {}, // .bind('fileuploadchange', func);
  231. // Callback for paste events to the pasteZone(s):
  232. // paste: function (e, data) {}, // .bind('fileuploadpaste', func);
  233. // Callback for drop events of the dropZone(s):
  234. // drop: function (e, data) {}, // .bind('fileuploaddrop', func);
  235. // Callback for dragover events of the dropZone(s):
  236. // dragover: function (e) {}, // .bind('fileuploaddragover', func);
  237. // Callback for the start of each chunk upload request:
  238. // chunksend: function (e, data) {}, // .bind('fileuploadchunksend', func);
  239. // Callback for successful chunk uploads:
  240. // chunkdone: function (e, data) {}, // .bind('fileuploadchunkdone', func);
  241. // Callback for failed (abort or error) chunk uploads:
  242. // chunkfail: function (e, data) {}, // .bind('fileuploadchunkfail', func);
  243. // Callback for completed (success, abort or error) chunk upload requests:
  244. // chunkalways: function (e, data) {}, // .bind('fileuploadchunkalways', func);
  245. // The plugin options are used as settings object for the ajax calls.
  246. // The following are jQuery ajax settings required for the file uploads:
  247. processData: false,
  248. contentType: false,
  249. cache: false
  250. },
  251. // A list of options that require reinitializing event listeners and/or
  252. // special initialization code:
  253. _specialOptions: [
  254. 'fileInput',
  255. 'dropZone',
  256. 'pasteZone',
  257. 'multipart',
  258. 'forceIframeTransport'
  259. ],
  260. _blobSlice: $.support.blobSlice && function () {
  261. var slice = this.slice || this.webkitSlice || this.mozSlice;
  262. return slice.apply(this, arguments);
  263. },
  264. _BitrateTimer: function () {
  265. this.timestamp = ((Date.now) ? Date.now() : (new Date()).getTime());
  266. this.loaded = 0;
  267. this.bitrate = 0;
  268. this.getBitrate = function (now, loaded, interval) {
  269. var timeDiff = now - this.timestamp;
  270. if (!this.bitrate || !interval || timeDiff > interval) {
  271. this.bitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8;
  272. this.loaded = loaded;
  273. this.timestamp = now;
  274. }
  275. return this.bitrate;
  276. };
  277. },
  278. _isXHRUpload: function (options) {
  279. return !options.forceIframeTransport &&
  280. ((!options.multipart && $.support.xhrFileUpload) ||
  281. $.support.xhrFormDataFileUpload);
  282. },
  283. _getFormData: function (options) {
  284. var formData;
  285. if ($.type(options.formData) === 'function') {
  286. return options.formData(options.form);
  287. }
  288. if ($.isArray(options.formData)) {
  289. return options.formData;
  290. }
  291. if ($.type(options.formData) === 'object') {
  292. formData = [];
  293. $.each(options.formData, function (name, value) {
  294. formData.push({name: name, value: value});
  295. });
  296. return formData;
  297. }
  298. return [];
  299. },
  300. _getTotal: function (files) {
  301. var total = 0;
  302. $.each(files, function (index, file) {
  303. total += file.size || 1;
  304. });
  305. return total;
  306. },
  307. _initProgressObject: function (obj) {
  308. var progress = {
  309. loaded: 0,
  310. total: 0,
  311. bitrate: 0
  312. };
  313. if (obj._progress) {
  314. $.extend(obj._progress, progress);
  315. } else {
  316. obj._progress = progress;
  317. }
  318. },
  319. _initResponseObject: function (obj) {
  320. var prop;
  321. if (obj._response) {
  322. for (prop in obj._response) {
  323. if (obj._response.hasOwnProperty(prop)) {
  324. delete obj._response[prop];
  325. }
  326. }
  327. } else {
  328. obj._response = {};
  329. }
  330. },
  331. _onProgress: function (e, data) {
  332. if (e.lengthComputable) {
  333. var now = ((Date.now) ? Date.now() : (new Date()).getTime()),
  334. loaded;
  335. if (data._time && data.progressInterval &&
  336. (now - data._time < data.progressInterval) &&
  337. e.loaded !== e.total) {
  338. return;
  339. }
  340. data._time = now;
  341. loaded = Math.floor(
  342. e.loaded / e.total * (data.chunkSize || data._progress.total)
  343. ) + (data.uploadedBytes || 0);
  344. // Add the difference from the previously loaded state
  345. // to the global loaded counter:
  346. this._progress.loaded += (loaded - data._progress.loaded);
  347. this._progress.bitrate = this._bitrateTimer.getBitrate(
  348. now,
  349. this._progress.loaded,
  350. data.bitrateInterval
  351. );
  352. data._progress.loaded = data.loaded = loaded;
  353. data._progress.bitrate = data.bitrate = data._bitrateTimer.getBitrate(
  354. now,
  355. loaded,
  356. data.bitrateInterval
  357. );
  358. // Trigger a custom progress event with a total data property set
  359. // to the file size(s) of the current upload and a loaded data
  360. // property calculated accordingly:
  361. this._trigger(
  362. 'progress',
  363. $.Event('progress', {delegatedEvent: e}),
  364. data
  365. );
  366. // Trigger a global progress event for all current file uploads,
  367. // including ajax calls queued for sequential file uploads:
  368. this._trigger(
  369. 'progressall',
  370. $.Event('progressall', {delegatedEvent: e}),
  371. this._progress
  372. );
  373. }
  374. },
  375. _initProgressListener: function (options) {
  376. var that = this,
  377. xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr();
  378. // Accesss to the native XHR object is required to add event listeners
  379. // for the upload progress event:
  380. if (xhr.upload) {
  381. $(xhr.upload).bind('progress', function (e) {
  382. var oe = e.originalEvent;
  383. // Make sure the progress event properties get copied over:
  384. e.lengthComputable = oe.lengthComputable;
  385. e.loaded = oe.loaded;
  386. e.total = oe.total;
  387. that._onProgress(e, options);
  388. });
  389. options.xhr = function () {
  390. return xhr;
  391. };
  392. }
  393. },
  394. _isInstanceOf: function (type, obj) {
  395. // Cross-frame instanceof check
  396. return Object.prototype.toString.call(obj) === '[object ' + type + ']';
  397. },
  398. _initXHRData: function (options) {
  399. var that = this,
  400. formData,
  401. file = options.files[0],
  402. // Ignore non-multipart setting if not supported:
  403. multipart = options.multipart || !$.support.xhrFileUpload,
  404. paramName = $.type(options.paramName) === 'array' ?
  405. options.paramName[0] : options.paramName;
  406. options.headers = $.extend({}, options.headers);
  407. if (options.contentRange) {
  408. options.headers['Content-Range'] = options.contentRange;
  409. }
  410. if (!multipart || options.blob || !this._isInstanceOf('File', file)) {
  411. options.headers['Content-Disposition'] = 'attachment; filename="' +
  412. encodeURI(file.name) + '"';
  413. }
  414. if (!multipart) {
  415. options.contentType = file.type || 'application/octet-stream';
  416. options.data = options.blob || file;
  417. } else if ($.support.xhrFormDataFileUpload) {
  418. if (options.postMessage) {
  419. // window.postMessage does not allow sending FormData
  420. // objects, so we just add the File/Blob objects to
  421. // the formData array and let the postMessage window
  422. // create the FormData object out of this array:
  423. formData = this._getFormData(options);
  424. if (options.blob) {
  425. formData.push({
  426. name: paramName,
  427. value: options.blob
  428. });
  429. } else {
  430. $.each(options.files, function (index, file) {
  431. formData.push({
  432. name: ($.type(options.paramName) === 'array' &&
  433. options.paramName[index]) || paramName,
  434. value: file
  435. });
  436. });
  437. }
  438. } else {
  439. if (that._isInstanceOf('FormData', options.formData)) {
  440. formData = options.formData;
  441. } else {
  442. formData = new FormData();
  443. $.each(this._getFormData(options), function (index, field) {
  444. formData.append(field.name, field.value);
  445. });
  446. }
  447. if (options.blob) {
  448. formData.append(paramName, options.blob, file.name);
  449. } else {
  450. $.each(options.files, function (index, file) {
  451. // This check allows the tests to run with
  452. // dummy objects:
  453. if (that._isInstanceOf('File', file) ||
  454. that._isInstanceOf('Blob', file)) {
  455. formData.append(
  456. ($.type(options.paramName) === 'array' &&
  457. options.paramName[index]) || paramName,
  458. file,
  459. file.uploadName || file.name
  460. );
  461. }
  462. });
  463. }
  464. }
  465. options.data = formData;
  466. }
  467. // Blob reference is not needed anymore, free memory:
  468. options.blob = null;
  469. },
  470. _initIframeSettings: function (options) {
  471. var targetHost = $('<a></a>').prop('href', options.url).prop('host');
  472. // Setting the dataType to iframe enables the iframe transport:
  473. options.dataType = 'iframe ' + (options.dataType || '');
  474. // The iframe transport accepts a serialized array as form data:
  475. options.formData = this._getFormData(options);
  476. // Add redirect url to form data on cross-domain uploads:
  477. if (options.redirect && targetHost && targetHost !== location.host) {
  478. options.formData.push({
  479. name: options.redirectParamName || 'redirect',
  480. value: options.redirect
  481. });
  482. }
  483. },
  484. _initDataSettings: function (options) {
  485. if (this._isXHRUpload(options)) {
  486. if (!this._chunkedUpload(options, true)) {
  487. if (!options.data) {
  488. this._initXHRData(options);
  489. }
  490. this._initProgressListener(options);
  491. }
  492. if (options.postMessage) {
  493. // Setting the dataType to postmessage enables the
  494. // postMessage transport:
  495. options.dataType = 'postmessage ' + (options.dataType || '');
  496. }
  497. } else {
  498. this._initIframeSettings(options);
  499. }
  500. },
  501. _getParamName: function (options) {
  502. var fileInput = $(options.fileInput),
  503. paramName = options.paramName;
  504. if (!paramName) {
  505. paramName = [];
  506. fileInput.each(function () {
  507. var input = $(this),
  508. name = input.prop('name') || 'files[]',
  509. i = (input.prop('files') || [1]).length;
  510. while (i) {
  511. paramName.push(name);
  512. i -= 1;
  513. }
  514. });
  515. if (!paramName.length) {
  516. paramName = [fileInput.prop('name') || 'files[]'];
  517. }
  518. } else if (!$.isArray(paramName)) {
  519. paramName = [paramName];
  520. }
  521. return paramName;
  522. },
  523. _initFormSettings: function (options) {
  524. // Retrieve missing options from the input field and the
  525. // associated form, if available:
  526. if (!options.form || !options.form.length) {
  527. options.form = $(options.fileInput.prop('form'));
  528. // If the given file input doesn't have an associated form,
  529. // use the default widget file input's form:
  530. if (!options.form.length) {
  531. options.form = $(this.options.fileInput.prop('form'));
  532. }
  533. }
  534. options.paramName = this._getParamName(options);
  535. if (!options.url) {
  536. options.url = options.form.prop('action') || location.href;
  537. }
  538. // The HTTP request method must be "POST" or "PUT":
  539. options.type = (options.type ||
  540. ($.type(options.form.prop('method')) === 'string' &&
  541. options.form.prop('method')) || ''
  542. ).toUpperCase();
  543. if (options.type !== 'POST' && options.type !== 'PUT' &&
  544. options.type !== 'PATCH') {
  545. options.type = 'POST';
  546. }
  547. if (!options.formAcceptCharset) {
  548. options.formAcceptCharset = options.form.attr('accept-charset');
  549. }
  550. },
  551. _getAJAXSettings: function (data) {
  552. var options = $.extend({}, this.options, data);
  553. this._initFormSettings(options);
  554. this._initDataSettings(options);
  555. return options;
  556. },
  557. // jQuery 1.6 doesn't provide .state(),
  558. // while jQuery 1.8+ removed .isRejected() and .isResolved():
  559. _getDeferredState: function (deferred) {
  560. if (deferred.state) {
  561. return deferred.state();
  562. }
  563. if (deferred.isResolved()) {
  564. return 'resolved';
  565. }
  566. if (deferred.isRejected()) {
  567. return 'rejected';
  568. }
  569. return 'pending';
  570. },
  571. // Maps jqXHR callbacks to the equivalent
  572. // methods of the given Promise object:
  573. _enhancePromise: function (promise) {
  574. promise.success = promise.done;
  575. promise.error = promise.fail;
  576. promise.complete = promise.always;
  577. return promise;
  578. },
  579. // Creates and returns a Promise object enhanced with
  580. // the jqXHR methods abort, success, error and complete:
  581. _getXHRPromise: function (resolveOrReject, context, args) {
  582. var dfd = $.Deferred(),
  583. promise = dfd.promise();
  584. context = context || this.options.context || promise;
  585. if (resolveOrReject === true) {
  586. dfd.resolveWith(context, args);
  587. } else if (resolveOrReject === false) {
  588. dfd.rejectWith(context, args);
  589. }
  590. promise.abort = dfd.promise;
  591. return this._enhancePromise(promise);
  592. },
  593. // Adds convenience methods to the data callback argument:
  594. _addConvenienceMethods: function (e, data) {
  595. var that = this,
  596. getPromise = function (args) {
  597. return $.Deferred().resolveWith(that, args).promise();
  598. };
  599. data.process = function (resolveFunc, rejectFunc) {
  600. if (resolveFunc || rejectFunc) {
  601. data._processQueue = this._processQueue =
  602. (this._processQueue || getPromise([this])).pipe(
  603. function () {
  604. if (data.errorThrown) {
  605. return $.Deferred()
  606. .rejectWith(that, [data]).promise();
  607. }
  608. return getPromise(arguments);
  609. }
  610. ).pipe(resolveFunc, rejectFunc);
  611. }
  612. return this._processQueue || getPromise([this]);
  613. };
  614. data.submit = function () {
  615. if (this.state() !== 'pending') {
  616. data.jqXHR = this.jqXHR =
  617. (that._trigger(
  618. 'submit',
  619. $.Event('submit', {delegatedEvent: e}),
  620. this
  621. ) !== false) && that._onSend(e, this);
  622. }
  623. return this.jqXHR || that._getXHRPromise();
  624. };
  625. data.abort = function () {
  626. if (this.jqXHR) {
  627. return this.jqXHR.abort();
  628. }
  629. this.errorThrown = 'abort';
  630. that._trigger('fail', null, this);
  631. return that._getXHRPromise(false);
  632. };
  633. data.state = function () {
  634. if (this.jqXHR) {
  635. return that._getDeferredState(this.jqXHR);
  636. }
  637. if (this._processQueue) {
  638. return that._getDeferredState(this._processQueue);
  639. }
  640. };
  641. data.processing = function () {
  642. return !this.jqXHR && this._processQueue && that
  643. ._getDeferredState(this._processQueue) === 'pending';
  644. };
  645. data.progress = function () {
  646. return this._progress;
  647. };
  648. data.response = function () {
  649. return this._response;
  650. };
  651. },
  652. // Parses the Range header from the server response
  653. // and returns the uploaded bytes:
  654. _getUploadedBytes: function (jqXHR) {
  655. var range = jqXHR.getResponseHeader('Range'),
  656. parts = range && range.split('-'),
  657. upperBytesPos = parts && parts.length > 1 &&
  658. parseInt(parts[1], 10);
  659. return upperBytesPos && upperBytesPos + 1;
  660. },
  661. // Uploads a file in multiple, sequential requests
  662. // by splitting the file up in multiple blob chunks.
  663. // If the second parameter is true, only tests if the file
  664. // should be uploaded in chunks, but does not invoke any
  665. // upload requests:
  666. _chunkedUpload: function (options, testOnly) {
  667. options.uploadedBytes = options.uploadedBytes || 0;
  668. var that = this,
  669. file = options.files[0],
  670. fs = file.size,
  671. ub = options.uploadedBytes,
  672. mcs = options.maxChunkSize || fs,
  673. slice = this._blobSlice,
  674. dfd = $.Deferred(),
  675. promise = dfd.promise(),
  676. jqXHR,
  677. upload;
  678. if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) ||
  679. options.data) {
  680. return false;
  681. }
  682. if (testOnly) {
  683. return true;
  684. }
  685. if (ub >= fs) {
  686. file.error = options.i18n('uploadedBytes');
  687. return this._getXHRPromise(
  688. false,
  689. options.context,
  690. [null, 'error', file.error]
  691. );
  692. }
  693. // The chunk upload method:
  694. upload = function () {
  695. // Clone the options object for each chunk upload:
  696. var o = $.extend({}, options),
  697. currentLoaded = o._progress.loaded;
  698. o.blob = slice.call(
  699. file,
  700. ub,
  701. ub + mcs,
  702. file.type
  703. );
  704. // Store the current chunk size, as the blob itself
  705. // will be dereferenced after data processing:
  706. o.chunkSize = o.blob.size;
  707. // Expose the chunk bytes position range:
  708. o.contentRange = 'bytes ' + ub + '-' +
  709. (ub + o.chunkSize - 1) + '/' + fs;
  710. // Process the upload data (the blob and potential form data):
  711. that._initXHRData(o);
  712. // Add progress listeners for this chunk upload:
  713. that._initProgressListener(o);
  714. jqXHR = ((that._trigger('chunksend', null, o) !== false && $.ajax(o)) ||
  715. that._getXHRPromise(false, o.context))
  716. .done(function (result, textStatus, jqXHR) {
  717. ub = that._getUploadedBytes(jqXHR) ||
  718. (ub + o.chunkSize);
  719. // Create a progress event if no final progress event
  720. // with loaded equaling total has been triggered
  721. // for this chunk:
  722. if (currentLoaded + o.chunkSize - o._progress.loaded) {
  723. that._onProgress($.Event('progress', {
  724. lengthComputable: true,
  725. loaded: ub - o.uploadedBytes,
  726. total: ub - o.uploadedBytes
  727. }), o);
  728. }
  729. options.uploadedBytes = o.uploadedBytes = ub;
  730. o.result = result;
  731. o.textStatus = textStatus;
  732. o.jqXHR = jqXHR;
  733. that._trigger('chunkdone', null, o);
  734. that._trigger('chunkalways', null, o);
  735. if (ub < fs) {
  736. // File upload not yet complete,
  737. // continue with the next chunk:
  738. upload();
  739. } else {
  740. dfd.resolveWith(
  741. o.context,
  742. [result, textStatus, jqXHR]
  743. );
  744. }
  745. })
  746. .fail(function (jqXHR, textStatus, errorThrown) {
  747. o.jqXHR = jqXHR;
  748. o.textStatus = textStatus;
  749. o.errorThrown = errorThrown;
  750. that._trigger('chunkfail', null, o);
  751. that._trigger('chunkalways', null, o);
  752. dfd.rejectWith(
  753. o.context,
  754. [jqXHR, textStatus, errorThrown]
  755. );
  756. });
  757. };
  758. this._enhancePromise(promise);
  759. promise.abort = function () {
  760. return jqXHR.abort();
  761. };
  762. upload();
  763. return promise;
  764. },
  765. _beforeSend: function (e, data) {
  766. if (this._active === 0) {
  767. // the start callback is triggered when an upload starts
  768. // and no other uploads are currently running,
  769. // equivalent to the global ajaxStart event:
  770. this._trigger('start');
  771. // Set timer for global bitrate progress calculation:
  772. this._bitrateTimer = new this._BitrateTimer();
  773. // Reset the global progress values:
  774. this._progress.loaded = this._progress.total = 0;
  775. this._progress.bitrate = 0;
  776. }
  777. // Make sure the container objects for the .response() and
  778. // .progress() methods on the data object are available
  779. // and reset to their initial state:
  780. this._initResponseObject(data);
  781. this._initProgressObject(data);
  782. data._progress.loaded = data.loaded = data.uploadedBytes || 0;
  783. data._progress.total = data.total = this._getTotal(data.files) || 1;
  784. data._progress.bitrate = data.bitrate = 0;
  785. this._active += 1;
  786. // Initialize the global progress values:
  787. this._progress.loaded += data.loaded;
  788. this._progress.total += data.total;
  789. },
  790. _onDone: function (result, textStatus, jqXHR, options) {
  791. var total = options._progress.total,
  792. response = options._response;
  793. if (options._progress.loaded < total) {
  794. // Create a progress event if no final progress event
  795. // with loaded equaling total has been triggered:
  796. this._onProgress($.Event('progress', {
  797. lengthComputable: true,
  798. loaded: total,
  799. total: total
  800. }), options);
  801. }
  802. response.result = options.result = result;
  803. response.textStatus = options.textStatus = textStatus;
  804. response.jqXHR = options.jqXHR = jqXHR;
  805. this._trigger('done', null, options);
  806. },
  807. _onFail: function (jqXHR, textStatus, errorThrown, options) {
  808. var response = options._response;
  809. if (options.recalculateProgress) {
  810. // Remove the failed (error or abort) file upload from
  811. // the global progress calculation:
  812. this._progress.loaded -= options._progress.loaded;
  813. this._progress.total -= options._progress.total;
  814. }
  815. response.jqXHR = options.jqXHR = jqXHR;
  816. response.textStatus = options.textStatus = textStatus;
  817. response.errorThrown = options.errorThrown = errorThrown;
  818. this._trigger('fail', null, options);
  819. },
  820. _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) {
  821. // jqXHRorResult, textStatus and jqXHRorError are added to the
  822. // options object via done and fail callbacks
  823. this._trigger('always', null, options);
  824. },
  825. _onSend: function (e, data) {
  826. if (!data.submit) {
  827. this._addConvenienceMethods(e, data);
  828. }
  829. var that = this,
  830. jqXHR,
  831. aborted,
  832. slot,
  833. pipe,
  834. options = that._getAJAXSettings(data),
  835. send = function () {
  836. that._sending += 1;
  837. // Set timer for bitrate progress calculation:
  838. options._bitrateTimer = new that._BitrateTimer();
  839. jqXHR = jqXHR || (
  840. ((aborted || that._trigger(
  841. 'send',
  842. $.Event('send', {delegatedEvent: e}),
  843. options
  844. ) === false) &&
  845. that._getXHRPromise(false, options.context, aborted)) ||
  846. that._chunkedUpload(options) || $.ajax(options)
  847. ).done(function (result, textStatus, jqXHR) {
  848. that._onDone(result, textStatus, jqXHR, options);
  849. }).fail(function (jqXHR, textStatus, errorThrown) {
  850. that._onFail(jqXHR, textStatus, errorThrown, options);
  851. }).always(function (jqXHRorResult, textStatus, jqXHRorError) {
  852. that._onAlways(
  853. jqXHRorResult,
  854. textStatus,
  855. jqXHRorError,
  856. options
  857. );
  858. that._sending -= 1;
  859. that._active -= 1;
  860. if (options.limitConcurrentUploads &&
  861. options.limitConcurrentUploads > that._sending) {
  862. // Start the next queued upload,
  863. // that has not been aborted:
  864. var nextSlot = that._slots.shift();
  865. while (nextSlot) {
  866. if (that._getDeferredState(nextSlot) === 'pending') {
  867. nextSlot.resolve();
  868. break;
  869. }
  870. nextSlot = that._slots.shift();
  871. }
  872. }
  873. if (that._active === 0) {
  874. // The stop callback is triggered when all uploads have
  875. // been completed, equivalent to the global ajaxStop event:
  876. that._trigger('stop');
  877. }
  878. });
  879. return jqXHR;
  880. };
  881. this._beforeSend(e, options);
  882. if (this.options.sequentialUploads ||
  883. (this.options.limitConcurrentUploads &&
  884. this.options.limitConcurrentUploads <= this._sending)) {
  885. if (this.options.limitConcurrentUploads > 1) {
  886. slot = $.Deferred();
  887. this._slots.push(slot);
  888. pipe = slot.pipe(send);
  889. } else {
  890. this._sequence = this._sequence.pipe(send, send);
  891. pipe = this._sequence;
  892. }
  893. // Return the piped Promise object, enhanced with an abort method,
  894. // which is delegated to the jqXHR object of the current upload,
  895. // and jqXHR callbacks mapped to the equivalent Promise methods:
  896. pipe.abort = function () {
  897. aborted = [undefined, 'abort', 'abort'];
  898. if (!jqXHR) {
  899. if (slot) {
  900. slot.rejectWith(options.context, aborted);
  901. }
  902. return send();
  903. }
  904. return jqXHR.abort();
  905. };
  906. return this._enhancePromise(pipe);
  907. }
  908. return send();
  909. },
  910. _onAdd: function (e, data) {
  911. var that = this,
  912. result = true,
  913. options = $.extend({}, this.options, data),
  914. files = data.files,
  915. filesLength = files.length,
  916. limit = options.limitMultiFileUploads,
  917. limitSize = options.limitMultiFileUploadSize,
  918. overhead = options.limitMultiFileUploadSizeOverhead,
  919. batchSize = 0,
  920. paramName = this._getParamName(options),
  921. paramNameSet,
  922. paramNameSlice,
  923. fileSet,
  924. i,
  925. j = 0;
  926. if (limitSize && (!filesLength || files[0].size === undefined)) {
  927. limitSize = undefined;
  928. }
  929. if (!(options.singleFileUploads || limit || limitSize) ||
  930. !this._isXHRUpload(options)) {
  931. fileSet = [files];
  932. paramNameSet = [paramName];
  933. } else if (!(options.singleFileUploads || limitSize) && limit) {
  934. fileSet = [];
  935. paramNameSet = [];
  936. for (i = 0; i < filesLength; i += limit) {
  937. fileSet.push(files.slice(i, i + limit));
  938. paramNameSlice = paramName.slice(i, i + limit);
  939. if (!paramNameSlice.length) {
  940. paramNameSlice = paramName;
  941. }
  942. paramNameSet.push(paramNameSlice);
  943. }
  944. } else if (!options.singleFileUploads && limitSize) {
  945. fileSet = [];
  946. paramNameSet = [];
  947. for (i = 0; i < filesLength; i = i + 1) {
  948. batchSize += files[i].size + overhead;
  949. if (i + 1 === filesLength ||
  950. ((batchSize + files[i + 1].size + overhead) > limitSize) ||
  951. (limit && i + 1 - j >= limit)) {
  952. fileSet.push(files.slice(j, i + 1));
  953. paramNameSlice = paramName.slice(j, i + 1);
  954. if (!paramNameSlice.length) {
  955. paramNameSlice = paramName;
  956. }
  957. paramNameSet.push(paramNameSlice);
  958. j = i + 1;
  959. batchSize = 0;
  960. }
  961. }
  962. } else {
  963. paramNameSet = paramName;
  964. }
  965. data.originalFiles = files;
  966. $.each(fileSet || files, function (index, element) {
  967. var newData = $.extend({}, data);
  968. newData.files = fileSet ? element : [element];
  969. newData.paramName = paramNameSet[index];
  970. that._initResponseObject(newData);
  971. that._initProgressObject(newData);
  972. that._addConvenienceMethods(e, newData);
  973. result = that._trigger(
  974. 'add',
  975. $.Event('add', {delegatedEvent: e}),
  976. newData
  977. );
  978. return result;
  979. });
  980. return result;
  981. },
  982. _replaceFileInput: function (data) {
  983. var input = data.fileInput,
  984. inputClone = input.clone(true);
  985. // Add a reference for the new cloned file input to the data argument:
  986. data.fileInputClone = inputClone;
  987. $('<form></form>').append(inputClone)[0].reset();
  988. // Detaching allows to insert the fileInput on another form
  989. // without loosing the file input value:
  990. input.after(inputClone).detach();
  991. // Avoid memory leaks with the detached file input:
  992. $.cleanData(input.unbind('remove'));
  993. // Replace the original file input element in the fileInput
  994. // elements set with the clone, which has been copied including
  995. // event handlers:
  996. this.options.fileInput = this.options.fileInput.map(function (i, el) {
  997. if (el === input[0]) {
  998. return inputClone[0];
  999. }
  1000. return el;
  1001. });
  1002. // If the widget has been initialized on the file input itself,
  1003. // override this.element with the file input clone:
  1004. if (input[0] === this.element[0]) {
  1005. this.element = inputClone;
  1006. }
  1007. },
  1008. _handleFileTreeEntry: function (entry, path) {
  1009. var that = this,
  1010. dfd = $.Deferred(),
  1011. errorHandler = function (e) {
  1012. if (e && !e.entry) {
  1013. e.entry = entry;
  1014. }
  1015. // Since $.when returns immediately if one
  1016. // Deferred is rejected, we use resolve instead.
  1017. // This allows valid files and invalid items
  1018. // to be returned together in one set:
  1019. dfd.resolve([e]);
  1020. },
  1021. successHandler = function (entries) {
  1022. that._handleFileTreeEntries(
  1023. entries,
  1024. path + entry.name + '/'
  1025. ).done(function (files) {
  1026. dfd.resolve(files);
  1027. }).fail(errorHandler);
  1028. },
  1029. readEntries = function () {
  1030. dirReader.readEntries(function (results) {
  1031. if (!results.length) {
  1032. successHandler(entries);
  1033. } else {
  1034. entries = entries.concat(results);
  1035. readEntries();
  1036. }
  1037. }, errorHandler);
  1038. },
  1039. dirReader, entries = [];
  1040. path = path || '';
  1041. if (entry.isFile) {
  1042. if (entry._file) {
  1043. // Workaround for Chrome bug #149735
  1044. entry._file.relativePath = path;
  1045. dfd.resolve(entry._file);
  1046. } else {
  1047. entry.file(function (file) {
  1048. file.relativePath = path;
  1049. dfd.resolve(file);
  1050. }, errorHandler);
  1051. }
  1052. } else if (entry.isDirectory) {
  1053. dirReader = entry.createReader();
  1054. readEntries();
  1055. } else {
  1056. // Return an empy list for file system items
  1057. // other than files or directories:
  1058. dfd.resolve([]);
  1059. }
  1060. return dfd.promise();
  1061. },
  1062. _handleFileTreeEntries: function (entries, path) {
  1063. var that = this;
  1064. return $.when.apply(
  1065. $,
  1066. $.map(entries, function (entry) {
  1067. return that._handleFileTreeEntry(entry, path);
  1068. })
  1069. ).pipe(function () {
  1070. return Array.prototype.concat.apply(
  1071. [],
  1072. arguments
  1073. );
  1074. });
  1075. },
  1076. _getDroppedFiles: function (dataTransfer) {
  1077. dataTransfer = dataTransfer || {};
  1078. var items = dataTransfer.items;
  1079. if (items && items.length && (items[0].webkitGetAsEntry ||
  1080. items[0].getAsEntry)) {
  1081. return this._handleFileTreeEntries(
  1082. $.map(items, function (item) {
  1083. var entry;
  1084. if (item.webkitGetAsEntry) {
  1085. entry = item.webkitGetAsEntry();
  1086. if (entry) {
  1087. // Workaround for Chrome bug #149735:
  1088. entry._file = item.getAsFile();
  1089. }
  1090. return entry;
  1091. }
  1092. return item.getAsEntry();
  1093. })
  1094. );
  1095. }
  1096. return $.Deferred().resolve(
  1097. $.makeArray(dataTransfer.files)
  1098. ).promise();
  1099. },
  1100. _getSingleFileInputFiles: function (fileInput) {
  1101. fileInput = $(fileInput);
  1102. var entries = fileInput.prop('webkitEntries') ||
  1103. fileInput.prop('entries'),
  1104. files,
  1105. value;
  1106. if (entries && entries.length) {
  1107. return this._handleFileTreeEntries(entries);
  1108. }
  1109. files = $.makeArray(fileInput.prop('files'));
  1110. if (!files.length) {
  1111. value = fileInput.prop('value');
  1112. if (!value) {
  1113. return $.Deferred().resolve([]).promise();
  1114. }
  1115. // If the files property is not available, the browser does not
  1116. // support the File API and we add a pseudo File object with
  1117. // the input value as name with path information removed:
  1118. files = [{name: value.replace(/^.*\\/, '')}];
  1119. } else if (files[0].name === undefined && files[0].fileName) {
  1120. // File normalization for Safari 4 and Firefox 3:
  1121. $.each(files, function (index, file) {
  1122. file.name = file.fileName;
  1123. file.size = file.fileSize;
  1124. });
  1125. }
  1126. return $.Deferred().resolve(files).promise();
  1127. },
  1128. _getFileInputFiles: function (fileInput) {
  1129. if (!(fileInput instanceof $) || fileInput.length === 1) {
  1130. return this._getSingleFileInputFiles(fileInput);
  1131. }
  1132. return $.when.apply(
  1133. $,
  1134. $.map(fileInput, this._getSingleFileInputFiles)
  1135. ).pipe(function () {
  1136. return Array.prototype.concat.apply(
  1137. [],
  1138. arguments
  1139. );
  1140. });
  1141. },
  1142. _onChange: function (e) {
  1143. var that = this,
  1144. data = {
  1145. fileInput: $(e.target),
  1146. form: $(e.target.form)
  1147. };
  1148. this._getFileInputFiles(data.fileInput).always(function (files) {
  1149. data.files = files;
  1150. if (that.options.replaceFileInput) {
  1151. that._replaceFileInput(data);
  1152. }
  1153. if (that._trigger(
  1154. 'change',
  1155. $.Event('change', {delegatedEvent: e}),
  1156. data
  1157. ) !== false) {
  1158. that._onAdd(e, data);
  1159. }
  1160. });
  1161. },
  1162. _onPaste: function (e) {
  1163. var items = e.originalEvent && e.originalEvent.clipboardData &&
  1164. e.originalEvent.clipboardData.items,
  1165. data = {files: []};
  1166. if (items && items.length) {
  1167. $.each(items, function (index, item) {
  1168. var file = item.getAsFile && item.getAsFile();
  1169. if (file) {
  1170. data.files.push(file);
  1171. }
  1172. });
  1173. if (this._trigger(
  1174. 'paste',
  1175. $.Event('paste', {delegatedEvent: e}),
  1176. data
  1177. ) !== false) {
  1178. this._onAdd(e, data);
  1179. }
  1180. }
  1181. },
  1182. _onDrop: function (e) {
  1183. e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer;
  1184. var that = this,
  1185. dataTransfer = e.dataTransfer,
  1186. data = {};
  1187. if (dataTransfer && dataTransfer.files && dataTransfer.files.length) {
  1188. e.preventDefault();
  1189. this._getDroppedFiles(dataTransfer).always(function (files) {
  1190. data.files = files;
  1191. if (that._trigger(
  1192. 'drop',
  1193. $.Event('drop', {delegatedEvent: e}),
  1194. data
  1195. ) !== false) {
  1196. that._onAdd(e, data);
  1197. }
  1198. });
  1199. }
  1200. },
  1201. _onDragOver: getDragHandler('dragover'),
  1202. _onDragEnter: getDragHandler('dragenter'),
  1203. _onDragLeave: getDragHandler('dragleave'),
  1204. _initEventHandlers: function () {
  1205. if (this._isXHRUpload(this.options)) {
  1206. this._on(this.options.dropZone, {
  1207. dragover: this._onDragOver,
  1208. drop: this._onDrop,
  1209. // event.preventDefault() on dragenter is required for IE10+:
  1210. dragenter: this._onDragEnter,
  1211. // dragleave is not required, but added for completeness:
  1212. dragleave: this._onDragLeave
  1213. });
  1214. this._on(this.options.pasteZone, {
  1215. paste: this._onPaste
  1216. });
  1217. }
  1218. if ($.support.fileInput) {
  1219. this._on(this.options.fileInput, {
  1220. change: this._onChange
  1221. });
  1222. }
  1223. },
  1224. _destroyEventHandlers: function () {
  1225. this._off(this.options.dropZone, 'dragenter dragleave dragover drop');
  1226. this._off(this.options.pasteZone, 'paste');
  1227. this._off(this.options.fileInput, 'change');
  1228. },
  1229. _setOption: function (key, value) {
  1230. var reinit = $.inArray(key, this._specialOptions) !== -1;
  1231. if (reinit) {
  1232. this._destroyEventHandlers();
  1233. }
  1234. this._super(key, value);
  1235. if (reinit) {
  1236. this._initSpecialOptions();
  1237. this._initEventHandlers();
  1238. }
  1239. },
  1240. _initSpecialOptions: function () {
  1241. var options = this.options;
  1242. if (options.fileInput === undefined) {
  1243. options.fileInput = this.element.is('input[type="file"]') ?
  1244. this.element : this.element.find('input[type="file"]');
  1245. } else if (!(options.fileInput instanceof $)) {
  1246. options.fileInput = $(options.fileInput);
  1247. }
  1248. if (!(options.dropZone instanceof $)) {
  1249. options.dropZone = $(options.dropZone);
  1250. }
  1251. if (!(options.pasteZone instanceof $)) {
  1252. options.pasteZone = $(options.pasteZone);
  1253. }
  1254. },
  1255. _getRegExp: function (str) {
  1256. var parts = str.split('/'),
  1257. modifiers = parts.pop();
  1258. parts.shift();
  1259. return new RegExp(parts.join('/'), modifiers);
  1260. },
  1261. _isRegExpOption: function (key, value) {
  1262. return key !== 'url' && $.type(value) === 'string' &&
  1263. /^\/.*\/[igm]{0,3}$/.test(value);
  1264. },
  1265. _initDataAttributes: function () {
  1266. var that = this,
  1267. options = this.options,
  1268. data = this.element.data();
  1269. // Initialize options set via HTML5 data-attributes:
  1270. $.each(
  1271. this.element[0].attributes,
  1272. function (index, attr) {
  1273. var key = attr.name.toLowerCase(),
  1274. value;
  1275. if (/^data-/.test(key)) {
  1276. // Convert hyphen-ated key to camelCase:
  1277. key = key.slice(5).replace(/-[a-z]/g, function (str) {
  1278. return str.charAt(1).toUpperCase();
  1279. });
  1280. value = data[key];
  1281. if (that._isRegExpOption(key, value)) {
  1282. value = that._getRegExp(value);
  1283. }
  1284. options[key] = value;
  1285. }
  1286. }
  1287. );
  1288. },
  1289. _create: function () {
  1290. this._initDataAttributes();
  1291. this._initSpecialOptions();
  1292. this._slots = [];
  1293. this._sequence = this._getXHRPromise(true);
  1294. this._sending = this._active = 0;
  1295. this._initProgressObject(this);
  1296. this._initEventHandlers();
  1297. },
  1298. // This method is exposed to the widget API and allows to query
  1299. // the number of active uploads:
  1300. active: function () {
  1301. return this._active;
  1302. },
  1303. // This method is exposed to the widget API and allows to query
  1304. // the widget upload progress.
  1305. // It returns an object with loaded, total and bitrate properties
  1306. // for the running uploads:
  1307. progress: function () {
  1308. return this._progress;
  1309. },
  1310. // This method is exposed to the widget API and allows adding files
  1311. // using the fileupload API. The data parameter accepts an object which
  1312. // must have a files property and can contain additional options:
  1313. // .fileupload('add', {files: filesList});
  1314. add: function (data) {
  1315. var that = this;
  1316. if (!data || this.options.disabled) {
  1317. return;
  1318. }
  1319. if (data.fileInput && !data.files) {
  1320. this._getFileInputFiles(data.fileInput).always(function (files) {
  1321. data.files = files;
  1322. that._onAdd(null, data);
  1323. });
  1324. } else {
  1325. data.files = $.makeArray(data.files);
  1326. this._onAdd(null, data);
  1327. }
  1328. },
  1329. // This method is exposed to the widget API and allows sending files
  1330. // using the fileupload API. The data parameter accepts an object which
  1331. // must have a files or fileInput property and can contain additional options:
  1332. // .fileupload('send', {files: filesList});
  1333. // The method returns a Promise object for the file upload call.
  1334. send: function (data) {
  1335. if (data && !this.options.disabled) {
  1336. if (data.fileInput && !data.files) {
  1337. var that = this,
  1338. dfd = $.Deferred(),
  1339. promise = dfd.promise(),
  1340. jqXHR,
  1341. aborted;
  1342. promise.abort = function () {
  1343. aborted = true;
  1344. if (jqXHR) {
  1345. return jqXHR.abort();
  1346. }
  1347. dfd.reject(null, 'abort', 'abort');
  1348. return promise;
  1349. };
  1350. this._getFileInputFiles(data.fileInput).always(
  1351. function (files) {
  1352. if (aborted) {
  1353. return;
  1354. }
  1355. if (!files.length) {
  1356. dfd.reject();
  1357. return;
  1358. }
  1359. data.files = files;
  1360. jqXHR = that._onSend(null, data);
  1361. jqXHR.then(
  1362. function (result, textStatus, jqXHR) {
  1363. dfd.resolve(result, textStatus, jqXHR);
  1364. },
  1365. function (jqXHR, textStatus, errorThrown) {
  1366. dfd.reject(jqXHR, textStatus, errorThrown);
  1367. }
  1368. );
  1369. }
  1370. );
  1371. return this._enhancePromise(promise);
  1372. }
  1373. data.files = $.makeArray(data.files);
  1374. if (data.files.length) {
  1375. return this._onSend(null, data);
  1376. }
  1377. }
  1378. return this._getXHRPromise(false, data && data.context);
  1379. }
  1380. });
  1381. }));