jquery.fileupload.js 62 KB

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