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