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