jquery.fileupload.js 55 KB

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