spree_image_multi_upload 1.0.0

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