fine_uploader 3.1.1 → 3.4.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -10,27 +10,40 @@ qq.FineUploaderBasic = function(o){
10
10
  request: {
11
11
  endpoint: '/server/upload',
12
12
  params: {},
13
- paramsInBody: false,
13
+ paramsInBody: true,
14
14
  customHeaders: {},
15
- forceMultipart: false,
16
- inputName: 'qqfile'
15
+ forceMultipart: true,
16
+ inputName: 'qqfile',
17
+ uuidName: 'qquuid',
18
+ totalFileSizeName: 'qqtotalfilesize'
17
19
  },
18
20
  validation: {
19
21
  allowedExtensions: [],
20
22
  sizeLimit: 0,
21
23
  minSizeLimit: 0,
24
+ itemLimit: 0,
22
25
  stopOnFirstInvalidFile: true
23
26
  },
24
27
  callbacks: {
25
- onSubmit: function(id, fileName){}, // return false to cancel submit
26
- onComplete: function(id, fileName, responseJSON){},
27
- onCancel: function(id, fileName){},
28
- onUpload: function(id, fileName, xhr){},
29
- onProgress: function(id, fileName, loaded, total){},
30
- onError: function(id, fileName, reason) {},
31
- onAutoRetry: function(id, fileName, attemptNumber) {},
32
- onManualRetry: function(id, fileName) {},
33
- onValidate: function(fileData) {} // return false to prevent upload
28
+ onSubmit: function(id, name){},
29
+ onSubmitted: function(id, name){},
30
+ onComplete: function(id, name, responseJSON){},
31
+ onCancel: function(id, name){},
32
+ onUpload: function(id, name){},
33
+ onUploadChunk: function(id, name, chunkData){},
34
+ onResume: function(id, fileName, chunkData){},
35
+ onProgress: function(id, name, loaded, total){},
36
+ onError: function(id, name, reason, maybeXhr) {},
37
+ onAutoRetry: function(id, name, attemptNumber) {},
38
+ onManualRetry: function(id, name) {},
39
+ onValidateBatch: function(fileOrBlobData) {},
40
+ onValidate: function(fileOrBlobData) {},
41
+ onSubmitDelete: function(id) {},
42
+ onDelete: function(id){},
43
+ onDeleteComplete: function(id, xhr, isError){},
44
+ onPasteReceived: function(blob) {
45
+ return new qq.Promise().success();
46
+ }
34
47
  },
35
48
  messages: {
36
49
  typeError: "{file} has an invalid extension. Valid extension(s): {extensions}.",
@@ -38,6 +51,8 @@ qq.FineUploaderBasic = function(o){
38
51
  minSizeError: "{file} is too small, minimum file size is {minSizeLimit}.",
39
52
  emptyError: "{file} is empty, please select files again without it.",
40
53
  noFilesError: "No files to upload.",
54
+ tooManyItemsError: "Too many items ({netItems}) would be uploaded. Item limit is {itemLimit}.",
55
+ retryFailTooManyItems: "Retry failed - you have reached your file limit.",
41
56
  onLeave: "The files are being uploaded, if you leave now the upload will be cancelled."
42
57
  },
43
58
  retry: {
@@ -49,6 +64,55 @@ qq.FineUploaderBasic = function(o){
49
64
  classes: {
50
65
  buttonHover: 'qq-upload-button-hover',
51
66
  buttonFocus: 'qq-upload-button-focus'
67
+ },
68
+ chunking: {
69
+ enabled: false,
70
+ partSize: 2000000,
71
+ paramNames: {
72
+ partIndex: 'qqpartindex',
73
+ partByteOffset: 'qqpartbyteoffset',
74
+ chunkSize: 'qqchunksize',
75
+ totalFileSize: 'qqtotalfilesize',
76
+ totalParts: 'qqtotalparts',
77
+ filename: 'qqfilename'
78
+ }
79
+ },
80
+ resume: {
81
+ enabled: false,
82
+ id: null,
83
+ cookiesExpireIn: 7, //days
84
+ paramNames: {
85
+ resuming: "qqresume"
86
+ }
87
+ },
88
+ formatFileName: function(fileOrBlobName) {
89
+ if (fileOrBlobName.length > 33) {
90
+ fileOrBlobName = fileOrBlobName.slice(0, 19) + '...' + fileOrBlobName.slice(-14);
91
+ }
92
+ return fileOrBlobName;
93
+ },
94
+ text: {
95
+ sizeSymbols: ['kB', 'MB', 'GB', 'TB', 'PB', 'EB']
96
+ },
97
+ deleteFile : {
98
+ enabled: false,
99
+ endpoint: '/server/upload',
100
+ customHeaders: {},
101
+ params: {}
102
+ },
103
+ cors: {
104
+ expected: false,
105
+ sendCredentials: false
106
+ },
107
+ blobs: {
108
+ defaultName: 'misc_data',
109
+ paramNames: {
110
+ name: 'qqblobname'
111
+ }
112
+ },
113
+ paste: {
114
+ targetElement: null,
115
+ defaultName: 'pasted_image'
52
116
  }
53
117
  };
54
118
 
@@ -56,23 +120,30 @@ qq.FineUploaderBasic = function(o){
56
120
  this._wrapCallbacks();
57
121
  this._disposeSupport = new qq.DisposeSupport();
58
122
 
59
- // number of files being uploaded
60
- this._filesInProgress = 0;
61
-
62
- this._storedFileIds = [];
63
-
123
+ this._filesInProgress = [];
124
+ this._storedIds = [];
64
125
  this._autoRetries = [];
65
126
  this._retryTimeouts = [];
66
127
  this._preventRetries = [];
128
+ this._netFilesUploadedOrQueued = 0;
129
+
130
+ this._paramsStore = this._createParamsStore("request");
131
+ this._deleteFileParamsStore = this._createParamsStore("deleteFile");
67
132
 
68
- this._paramsStore = this._createParamsStore();
133
+ this._endpointStore = this._createEndpointStore("request");
134
+ this._deleteFileEndpointStore = this._createEndpointStore("deleteFile");
69
135
 
70
136
  this._handler = this._createUploadHandler();
137
+ this._deleteHandler = this._createDeleteHandler();
71
138
 
72
139
  if (this._options.button){
73
140
  this._button = this._createUploadButton(this._options.button);
74
141
  }
75
142
 
143
+ if (this._options.paste.targetElement) {
144
+ this._pasteHandler = this._createPasteHandler();
145
+ }
146
+
76
147
  this._preventLeaveInProgress();
77
148
  };
78
149
 
@@ -86,29 +157,52 @@ qq.FineUploaderBasic.prototype = {
86
157
 
87
158
  }
88
159
  },
89
- setParams: function(params, fileId){
90
- if (fileId === undefined) {
160
+ setParams: function(params, id) {
161
+ /*jshint eqeqeq: true, eqnull: true*/
162
+ if (id == null) {
91
163
  this._options.request.params = params;
92
164
  }
93
165
  else {
94
- this._paramsStore.setParams(params, fileId);
166
+ this._paramsStore.setParams(params, id);
167
+ }
168
+ },
169
+ setDeleteFileParams: function(params, id) {
170
+ /*jshint eqeqeq: true, eqnull: true*/
171
+ if (id == null) {
172
+ this._options.deleteFile.params = params;
173
+ }
174
+ else {
175
+ this._deleteFileParamsStore.setParams(params, id);
176
+ }
177
+ },
178
+ setEndpoint: function(endpoint, id) {
179
+ /*jshint eqeqeq: true, eqnull: true*/
180
+ if (id == null) {
181
+ this._options.request.endpoint = endpoint;
182
+ }
183
+ else {
184
+ this._endpointStore.setEndpoint(endpoint, id);
95
185
  }
96
186
  },
97
187
  getInProgress: function(){
98
- return this._filesInProgress;
188
+ return this._filesInProgress.length;
99
189
  },
100
190
  uploadStoredFiles: function(){
101
191
  "use strict";
102
- while(this._storedFileIds.length) {
103
- this._filesInProgress++;
104
- this._handler.upload(this._storedFileIds.shift());
192
+ var idToUpload;
193
+
194
+ while(this._storedIds.length) {
195
+ idToUpload = this._storedIds.shift();
196
+ this._filesInProgress.push(idToUpload);
197
+ this._handler.upload(idToUpload);
105
198
  }
106
199
  },
107
200
  clearStoredFiles: function(){
108
- this._storedFileIds = [];
201
+ this._storedIds = [];
109
202
  },
110
203
  retry: function(id) {
111
204
  if (this._onBeforeManualRetry(id)) {
205
+ this._netFilesUploadedOrQueued++;
112
206
  this._handler.retry(id);
113
207
  return true;
114
208
  }
@@ -116,32 +210,49 @@ qq.FineUploaderBasic.prototype = {
116
210
  return false;
117
211
  }
118
212
  },
119
- cancel: function(fileId) {
120
- this._handler.cancel(fileId);
213
+ cancel: function(id) {
214
+ this._handler.cancel(id);
215
+ },
216
+ cancelAll: function() {
217
+ var storedIdsCopy = [],
218
+ self = this;
219
+
220
+ qq.extend(storedIdsCopy, this._storedIds);
221
+ qq.each(storedIdsCopy, function(idx, storedFileId) {
222
+ self.cancel(storedFileId);
223
+ });
224
+
225
+ this._handler.cancelAll();
121
226
  },
122
227
  reset: function() {
123
228
  this.log("Resetting uploader...");
124
229
  this._handler.reset();
125
- this._filesInProgress = 0;
126
- this._storedFileIds = [];
230
+ this._filesInProgress = [];
231
+ this._storedIds = [];
127
232
  this._autoRetries = [];
128
233
  this._retryTimeouts = [];
129
234
  this._preventRetries = [];
130
235
  this._button.reset();
131
236
  this._paramsStore.reset();
237
+ this._endpointStore.reset();
238
+ this._netFilesUploadedOrQueued = 0;
239
+
240
+ if (this._pasteHandler) {
241
+ this._pasteHandler.reset();
242
+ }
132
243
  },
133
- addFiles: function(filesOrInputs) {
244
+ addFiles: function(filesBlobDataOrInputs) {
134
245
  var self = this,
135
246
  verifiedFilesOrInputs = [],
136
247
  index, fileOrInput;
137
248
 
138
- if (filesOrInputs) {
139
- if (!window.FileList || !filesOrInputs instanceof FileList) {
140
- filesOrInputs = [].concat(filesOrInputs);
249
+ if (filesBlobDataOrInputs) {
250
+ if (!window.FileList || !(filesBlobDataOrInputs instanceof FileList)) {
251
+ filesBlobDataOrInputs = [].concat(filesBlobDataOrInputs);
141
252
  }
142
253
 
143
- for (index = 0; index < filesOrInputs.length; index+=1) {
144
- fileOrInput = filesOrInputs[index];
254
+ for (index = 0; index < filesBlobDataOrInputs.length; index+=1) {
255
+ fileOrInput = filesBlobDataOrInputs[index];
145
256
 
146
257
  if (qq.isFileOrInput(fileOrInput)) {
147
258
  verifiedFilesOrInputs.push(fileOrInput);
@@ -152,8 +263,65 @@ qq.FineUploaderBasic.prototype = {
152
263
  }
153
264
 
154
265
  this.log('Processing ' + verifiedFilesOrInputs.length + ' files or inputs...');
155
- this._uploadFileList(verifiedFilesOrInputs);
266
+ this._uploadFileOrBlobDataList(verifiedFilesOrInputs);
267
+ }
268
+ },
269
+ addBlobs: function(blobDataOrArray) {
270
+ if (blobDataOrArray) {
271
+ var blobDataArray = [].concat(blobDataOrArray),
272
+ verifiedBlobDataList = [],
273
+ self = this;
274
+
275
+ qq.each(blobDataArray, function(idx, blobData) {
276
+ if (qq.isBlob(blobData) && !qq.isFileOrInput(blobData)) {
277
+ verifiedBlobDataList.push({
278
+ blob: blobData,
279
+ name: self._options.blobs.defaultName
280
+ });
281
+ }
282
+ else if (qq.isObject(blobData) && blobData.blob && blobData.name) {
283
+ verifiedBlobDataList.push(blobData);
284
+ }
285
+ else {
286
+ self.log("addBlobs: entry at index " + idx + " is not a Blob or a BlobData object", "error");
287
+ }
288
+ });
289
+
290
+ this._uploadFileOrBlobDataList(verifiedBlobDataList);
156
291
  }
292
+ else {
293
+ this.log("undefined or non-array parameter passed into addBlobs", "error");
294
+ }
295
+ },
296
+ getUuid: function(id) {
297
+ return this._handler.getUuid(id);
298
+ },
299
+ getResumableFilesData: function() {
300
+ return this._handler.getResumableFilesData();
301
+ },
302
+ getSize: function(id) {
303
+ return this._handler.getSize(id);
304
+ },
305
+ getName: function(id) {
306
+ return this._handler.getName(id);
307
+ },
308
+ getFile: function(fileOrBlobId) {
309
+ return this._handler.getFile(fileOrBlobId);
310
+ },
311
+ deleteFile: function(id) {
312
+ this._onSubmitDelete(id);
313
+ },
314
+ setDeleteFileEndpoint: function(endpoint, id) {
315
+ /*jshint eqeqeq: true, eqnull: true*/
316
+ if (id == null) {
317
+ this._options.deleteFile.endpoint = endpoint;
318
+ }
319
+ else {
320
+ this._deleteFileEndpointStore.setEndpoint(endpoint, id);
321
+ }
322
+ },
323
+ getPromissoryCallbackNames: function() {
324
+ return ["onPasteReceived"];
157
325
  },
158
326
  _createUploadButton: function(element){
159
327
  var self = this;
@@ -173,52 +341,59 @@ qq.FineUploaderBasic.prototype = {
173
341
  return button;
174
342
  },
175
343
  _createUploadHandler: function(){
176
- var self = this,
177
- handlerClass;
178
-
179
- if(qq.isXhrUploadSupported()){
180
- handlerClass = 'UploadHandlerXhr';
181
- } else {
182
- handlerClass = 'UploadHandlerForm';
183
- }
344
+ var self = this;
184
345
 
185
- var handler = new qq[handlerClass]({
346
+ return new qq.UploadHandler({
186
347
  debug: this._options.debug,
187
- endpoint: this._options.request.endpoint,
188
348
  forceMultipart: this._options.request.forceMultipart,
189
349
  maxConnections: this._options.maxConnections,
190
350
  customHeaders: this._options.request.customHeaders,
191
351
  inputName: this._options.request.inputName,
352
+ uuidParamName: this._options.request.uuidName,
353
+ totalFileSizeParamName: this._options.request.totalFileSizeName,
354
+ cors: this._options.cors,
192
355
  demoMode: this._options.demoMode,
193
- log: this.log,
194
356
  paramsInBody: this._options.request.paramsInBody,
195
357
  paramsStore: this._paramsStore,
196
- onProgress: function(id, fileName, loaded, total){
197
- self._onProgress(id, fileName, loaded, total);
198
- self._options.callbacks.onProgress(id, fileName, loaded, total);
358
+ endpointStore: this._endpointStore,
359
+ chunking: this._options.chunking,
360
+ resume: this._options.resume,
361
+ blobs: this._options.blobs,
362
+ log: function(str, level) {
363
+ self.log(str, level);
199
364
  },
200
- onComplete: function(id, fileName, result, xhr){
201
- self._onComplete(id, fileName, result, xhr);
202
- self._options.callbacks.onComplete(id, fileName, result);
365
+ onProgress: function(id, name, loaded, total){
366
+ self._onProgress(id, name, loaded, total);
367
+ self._options.callbacks.onProgress(id, name, loaded, total);
203
368
  },
204
- onCancel: function(id, fileName){
205
- self._onCancel(id, fileName);
206
- self._options.callbacks.onCancel(id, fileName);
369
+ onComplete: function(id, name, result, xhr){
370
+ self._onComplete(id, name, result, xhr);
371
+ self._options.callbacks.onComplete(id, name, result);
207
372
  },
208
- onUpload: function(id, fileName, xhr){
209
- self._onUpload(id, fileName, xhr);
210
- self._options.callbacks.onUpload(id, fileName, xhr);
373
+ onCancel: function(id, name){
374
+ self._onCancel(id, name);
375
+ self._options.callbacks.onCancel(id, name);
211
376
  },
212
- onAutoRetry: function(id, fileName, responseJSON, xhr) {
377
+ onUpload: function(id, name){
378
+ self._onUpload(id, name);
379
+ self._options.callbacks.onUpload(id, name);
380
+ },
381
+ onUploadChunk: function(id, name, chunkData){
382
+ self._options.callbacks.onUploadChunk(id, name, chunkData);
383
+ },
384
+ onResume: function(id, name, chunkData) {
385
+ return self._options.callbacks.onResume(id, name, chunkData);
386
+ },
387
+ onAutoRetry: function(id, name, responseJSON, xhr) {
213
388
  self._preventRetries[id] = responseJSON[self._options.retry.preventRetryResponseProperty];
214
389
 
215
- if (self._shouldAutoRetry(id, fileName, responseJSON)) {
216
- self._maybeParseAndSendUploadError(id, fileName, responseJSON, xhr);
217
- self._options.callbacks.onAutoRetry(id, fileName, self._autoRetries[id] + 1);
218
- self._onBeforeAutoRetry(id, fileName);
390
+ if (self._shouldAutoRetry(id, name, responseJSON)) {
391
+ self._maybeParseAndSendUploadError(id, name, responseJSON, xhr);
392
+ self._options.callbacks.onAutoRetry(id, name, self._autoRetries[id] + 1);
393
+ self._onBeforeAutoRetry(id, name);
219
394
 
220
395
  self._retryTimeouts[id] = setTimeout(function() {
221
- self._onAutoRetry(id, fileName, responseJSON)
396
+ self._onAutoRetry(id, name, responseJSON)
222
397
  }, self._options.retry.autoAttemptDelay * 1000);
223
398
 
224
399
  return true;
@@ -228,14 +403,79 @@ qq.FineUploaderBasic.prototype = {
228
403
  }
229
404
  }
230
405
  });
406
+ },
407
+ _createDeleteHandler: function() {
408
+ var self = this;
409
+
410
+ return new qq.DeleteFileAjaxRequestor({
411
+ maxConnections: this._options.maxConnections,
412
+ customHeaders: this._options.deleteFile.customHeaders,
413
+ paramsStore: this._deleteFileParamsStore,
414
+ endpointStore: this._deleteFileEndpointStore,
415
+ demoMode: this._options.demoMode,
416
+ cors: this._options.cors,
417
+ log: function(str, level) {
418
+ self.log(str, level);
419
+ },
420
+ onDelete: function(id) {
421
+ self._onDelete(id);
422
+ self._options.callbacks.onDelete(id);
423
+ },
424
+ onDeleteComplete: function(id, xhr, isError) {
425
+ self._onDeleteComplete(id, xhr, isError);
426
+ self._options.callbacks.onDeleteComplete(id, xhr, isError);
427
+ }
428
+
429
+ });
430
+ },
431
+ _createPasteHandler: function() {
432
+ var self = this;
231
433
 
232
- return handler;
434
+ return new qq.PasteSupport({
435
+ targetElement: this._options.paste.targetElement,
436
+ callbacks: {
437
+ log: function(str, level) {
438
+ self.log(str, level);
439
+ },
440
+ pasteReceived: function(blob) {
441
+ var pasteReceivedCallback = self._options.callbacks.onPasteReceived,
442
+ promise = pasteReceivedCallback(blob);
443
+
444
+ if (promise.then) {
445
+ promise.then(function(successData) {
446
+ self._handlePasteSuccess(blob, successData);
447
+ }, function(failureData) {
448
+ self.log("Ignoring pasted image per paste received callback. Reason = '" + failureData + "'");
449
+ });
450
+ }
451
+ else {
452
+ self.log("Promise contract not fulfilled in pasteReceived callback handler! Ignoring pasted item.", "error");
453
+ }
454
+ }
455
+ }
456
+ });
457
+ },
458
+ _handlePasteSuccess: function(blob, extSuppliedName) {
459
+ var extension = blob.type.split("/")[1],
460
+ name = extSuppliedName;
461
+
462
+ /*jshint eqeqeq: true, eqnull: true*/
463
+ if (name == null) {
464
+ name = this._options.paste.defaultName;
465
+ }
466
+
467
+ name += '.' + extension;
468
+
469
+ this.addBlobs({
470
+ name: name,
471
+ blob: blob
472
+ });
233
473
  },
234
474
  _preventLeaveInProgress: function(){
235
475
  var self = this;
236
476
 
237
477
  this._disposeSupport.attach(window, 'beforeunload', function(e){
238
- if (!self._filesInProgress){return;}
478
+ if (!self._filesInProgress.length){return;}
239
479
 
240
480
  var e = e || window.event;
241
481
  // for ie, ff
@@ -244,61 +484,105 @@ qq.FineUploaderBasic.prototype = {
244
484
  return self._options.messages.onLeave;
245
485
  });
246
486
  },
247
- _onSubmit: function(id, fileName){
487
+ _onSubmit: function(id, name) {
488
+ this._netFilesUploadedOrQueued++;
489
+
248
490
  if (this._options.autoUpload) {
249
- this._filesInProgress++;
491
+ this._filesInProgress.push(id);
250
492
  }
251
493
  },
252
- _onProgress: function(id, fileName, loaded, total){
494
+ _onProgress: function(id, name, loaded, total){
253
495
  },
254
- _onComplete: function(id, fileName, result, xhr){
255
- this._filesInProgress--;
256
- this._maybeParseAndSendUploadError(id, fileName, result, xhr);
496
+ _onComplete: function(id, name, result, xhr) {
497
+ if (!result.success) {
498
+ this._netFilesUploadedOrQueued--;
499
+ }
500
+
501
+ this._removeFromFilesInProgress(id);
502
+ this._maybeParseAndSendUploadError(id, name, result, xhr);
257
503
  },
258
- _onCancel: function(id, fileName){
504
+ _onCancel: function(id, name){
505
+ this._netFilesUploadedOrQueued--;
506
+
507
+ this._removeFromFilesInProgress(id);
508
+
259
509
  clearTimeout(this._retryTimeouts[id]);
260
510
 
261
- var storedFileIndex = qq.indexOf(this._storedFileIds, id);
262
- if (this._options.autoUpload || storedFileIndex < 0) {
263
- this._filesInProgress--;
511
+ var storedItemIndex = qq.indexOf(this._storedIds, id);
512
+ if (!this._options.autoUpload && storedItemIndex >= 0) {
513
+ this._storedIds.splice(storedItemIndex, 1);
264
514
  }
265
- else if (!this._options.autoUpload) {
266
- this._storedFileIds.splice(storedFileIndex, 1);
515
+ },
516
+ _isDeletePossible: function() {
517
+ return (this._options.deleteFile.enabled &&
518
+ (!this._options.cors.expected ||
519
+ (this._options.cors.expected && (qq.ie10() || !qq.ie()))
520
+ )
521
+ );
522
+ },
523
+ _onSubmitDelete: function(id) {
524
+ if (this._isDeletePossible()) {
525
+ if (this._options.callbacks.onSubmitDelete(id) !== false) {
526
+ this._deleteHandler.sendDelete(id, this.getUuid(id));
527
+ }
528
+ }
529
+ else {
530
+ this.log("Delete request ignored for ID " + id + ", delete feature is disabled or request not possible " +
531
+ "due to CORS on a user agent that does not support pre-flighting.", "warn");
532
+ return false;
267
533
  }
268
534
  },
269
- _onUpload: function(id, fileName, xhr){
535
+ _onDelete: function(fileId) {},
536
+ _onDeleteComplete: function(id, xhr, isError) {
537
+ var name = this._handler.getName(id);
538
+
539
+ if (isError) {
540
+ this.log("Delete request for '" + name + "' has failed.", "error");
541
+ this._options.callbacks.onError(id, name, "Delete request failed with response code " + xhr.status, xhr);
542
+ }
543
+ else {
544
+ this._netFilesUploadedOrQueued--;
545
+ this.log("Delete request for '" + name + "' has succeeded.");
546
+ }
270
547
  },
548
+ _removeFromFilesInProgress: function(id) {
549
+ var index = qq.indexOf(this._filesInProgress, id);
550
+ if (index >= 0) {
551
+ this._filesInProgress.splice(index, 1);
552
+ }
553
+ },
554
+ _onUpload: function(id, name){},
271
555
  _onInputChange: function(input){
272
- if (this._handler instanceof qq.UploadHandlerXhr){
556
+ if (qq.isXhrUploadSupported()){
273
557
  this.addFiles(input.files);
274
558
  } else {
275
- if (this._validateFile(input)){
276
- this.addFiles(input);
277
- }
559
+ this.addFiles(input);
278
560
  }
279
561
  this._button.reset();
280
562
  },
281
- _onBeforeAutoRetry: function(id, fileName) {
282
- this.log("Waiting " + this._options.retry.autoAttemptDelay + " seconds before retrying " + fileName + "...");
563
+ _onBeforeAutoRetry: function(id, name) {
564
+ this.log("Waiting " + this._options.retry.autoAttemptDelay + " seconds before retrying " + name + "...");
283
565
  },
284
- _onAutoRetry: function(id, fileName, responseJSON) {
285
- this.log("Retrying " + fileName + "...");
566
+ _onAutoRetry: function(id, name, responseJSON) {
567
+ this.log("Retrying " + name + "...");
286
568
  this._autoRetries[id]++;
287
569
  this._handler.retry(id);
288
570
  },
289
- _shouldAutoRetry: function(id, fileName, responseJSON) {
571
+ _shouldAutoRetry: function(id, name, responseJSON) {
290
572
  if (!this._preventRetries[id] && this._options.retry.enableAuto) {
291
573
  if (this._autoRetries[id] === undefined) {
292
574
  this._autoRetries[id] = 0;
293
575
  }
294
576
 
295
- return this._autoRetries[id] < this._options.retry.maxAutoAttempts
577
+ return this._autoRetries[id] < this._options.retry.maxAutoAttempts;
296
578
  }
297
579
 
298
580
  return false;
299
581
  },
300
582
  //return false if we should not attempt the requested retry
301
583
  _onBeforeManualRetry: function(id) {
584
+ var itemLimit = this._options.validation.itemLimit;
585
+
302
586
  if (this._preventRetries[id]) {
303
587
  this.log("Retries are forbidden for id " + id, 'warn');
304
588
  return false;
@@ -310,8 +594,13 @@ qq.FineUploaderBasic.prototype = {
310
594
  return false;
311
595
  }
312
596
 
597
+ if (itemLimit > 0 && this._netFilesUploadedOrQueued+1 > itemLimit) {
598
+ this._itemError("retryFailTooManyItems", "");
599
+ return false;
600
+ }
601
+
313
602
  this.log("Retrying upload for '" + fileName + "' (id: " + id + ")...");
314
- this._filesInProgress++;
603
+ this._filesInProgress.push(id);
315
604
  return true;
316
605
  }
317
606
  else {
@@ -319,29 +608,28 @@ qq.FineUploaderBasic.prototype = {
319
608
  return false;
320
609
  }
321
610
  },
322
- _maybeParseAndSendUploadError: function(id, fileName, response, xhr) {
611
+ _maybeParseAndSendUploadError: function(id, name, response, xhr) {
323
612
  //assuming no one will actually set the response code to something other than 200 and still set 'success' to true
324
613
  if (!response.success){
325
614
  if (xhr && xhr.status !== 200 && !response.error) {
326
- this._options.callbacks.onError(id, fileName, "XHR returned response code " + xhr.status);
615
+ this._options.callbacks.onError(id, name, "XHR returned response code " + xhr.status, xhr);
327
616
  }
328
617
  else {
329
618
  var errorReason = response.error ? response.error : "Upload failure reason unknown";
330
- this._options.callbacks.onError(id, fileName, errorReason);
619
+ this._options.callbacks.onError(id, name, errorReason, xhr);
331
620
  }
332
621
  }
333
622
  },
334
- _uploadFileList: function(files){
335
- var validationDescriptors, index, batchInvalid;
336
-
337
- validationDescriptors = this._getValidationDescriptors(files);
338
- batchInvalid = this._options.callbacks.onValidate(validationDescriptors) === false;
623
+ _uploadFileOrBlobDataList: function(fileOrBlobDataList){
624
+ var index,
625
+ validationDescriptors = this._getValidationDescriptors(fileOrBlobDataList),
626
+ batchValid = this._isBatchValid(validationDescriptors);
339
627
 
340
- if (!batchInvalid) {
341
- if (files.length > 0) {
342
- for (index = 0; index < files.length; index++){
343
- if (this._validateFile(files[index])){
344
- this._uploadFile(files[index]);
628
+ if (batchValid) {
629
+ if (fileOrBlobDataList.length > 0) {
630
+ for (index = 0; index < fileOrBlobDataList.length; index++){
631
+ if (this._validateFileOrBlobData(fileOrBlobDataList[index])){
632
+ this._upload(fileOrBlobDataList[index]);
345
633
  } else {
346
634
  if (this._options.validation.stopOnFirstInvalidFile){
347
635
  return;
@@ -350,94 +638,141 @@ qq.FineUploaderBasic.prototype = {
350
638
  }
351
639
  }
352
640
  else {
353
- this._error('noFilesError', "");
641
+ this._itemError("noFilesError", "");
354
642
  }
355
643
  }
356
644
  },
357
- _uploadFile: function(fileContainer){
358
- var id = this._handler.add(fileContainer);
359
- var fileName = this._handler.getName(id);
645
+ _upload: function(blobOrFileContainer){
646
+ var id = this._handler.add(blobOrFileContainer);
647
+ var name = this._handler.getName(id);
648
+
649
+ if (this._options.callbacks.onSubmit(id, name) !== false) {
650
+ this._onSubmit(id, name);
651
+ this._options.callbacks.onSubmitted(id, name);
360
652
 
361
- if (this._options.callbacks.onSubmit(id, fileName) !== false){
362
- this._onSubmit(id, fileName);
363
653
  if (this._options.autoUpload) {
364
654
  this._handler.upload(id);
365
655
  }
366
656
  else {
367
- this._storeFileForLater(id);
657
+ this._storeForLater(id);
368
658
  }
369
659
  }
370
660
  },
371
- _storeFileForLater: function(id) {
372
- this._storedFileIds.push(id);
661
+ _storeForLater: function(id) {
662
+ this._storedIds.push(id);
663
+ },
664
+ _isBatchValid: function(validationDescriptors) {
665
+ //first, defer the check to the callback (ask the integrator)
666
+ var errorMessage,
667
+ itemLimit = this._options.validation.itemLimit,
668
+ proposedNetFilesUploadedOrQueued = this._netFilesUploadedOrQueued + validationDescriptors.length,
669
+ batchValid = this._options.callbacks.onValidateBatch(validationDescriptors) !== false;
670
+
671
+ //if the callback hasn't rejected the batch, run some internal tests on the batch next
672
+ if (batchValid) {
673
+ if (itemLimit === 0 || proposedNetFilesUploadedOrQueued <= itemLimit) {
674
+ batchValid = true;
675
+ }
676
+ else {
677
+ batchValid = false;
678
+ errorMessage = this._options.messages.tooManyItemsError
679
+ .replace(/\{netItems\}/g, proposedNetFilesUploadedOrQueued)
680
+ .replace(/\{itemLimit\}/g, itemLimit);
681
+ this._batchError(errorMessage);
682
+ }
683
+ }
684
+
685
+ return batchValid;
373
686
  },
374
- _validateFile: function(file){
687
+ _validateFileOrBlobData: function(fileOrBlobData){
375
688
  var validationDescriptor, name, size;
376
689
 
377
- validationDescriptor = this._getValidationDescriptor(file);
690
+ validationDescriptor = this._getValidationDescriptor(fileOrBlobData);
378
691
  name = validationDescriptor.name;
379
692
  size = validationDescriptor.size;
380
693
 
381
- if (this._options.callbacks.onValidate([validationDescriptor]) === false) {
694
+ if (this._options.callbacks.onValidate(validationDescriptor) === false) {
382
695
  return false;
383
696
  }
384
697
 
385
- if (!this._isAllowedExtension(name)){
386
- this._error('typeError', name);
698
+ if (qq.isFileOrInput(fileOrBlobData) && !this._isAllowedExtension(name)){
699
+ this._itemError('typeError', name);
387
700
  return false;
388
701
 
389
702
  }
390
703
  else if (size === 0){
391
- this._error('emptyError', name);
704
+ this._itemError('emptyError', name);
392
705
  return false;
393
706
 
394
707
  }
395
708
  else if (size && this._options.validation.sizeLimit && size > this._options.validation.sizeLimit){
396
- this._error('sizeError', name);
709
+ this._itemError('sizeError', name);
397
710
  return false;
398
711
 
399
712
  }
400
713
  else if (size && size < this._options.validation.minSizeLimit){
401
- this._error('minSizeError', name);
714
+ this._itemError('minSizeError', name);
402
715
  return false;
403
716
  }
404
717
 
405
718
  return true;
406
719
  },
407
- _error: function(code, fileName){
408
- var message = this._options.messages[code];
720
+ _itemError: function(code, name) {
721
+ var message = this._options.messages[code],
722
+ allowedExtensions = [],
723
+ extensionsForMessage;
724
+
409
725
  function r(name, replacement){ message = message.replace(name, replacement); }
410
726
 
411
- var extensions = this._options.validation.allowedExtensions.join(', ');
727
+ qq.each(this._options.validation.allowedExtensions, function(idx, allowedExtension) {
728
+ /**
729
+ * If an argument is not a string, ignore it. Added when a possible issue with MooTools hijacking the
730
+ * `allowedExtensions` array was discovered. See case #735 in the issue tracker for more details.
731
+ */
732
+ if (qq.isString(allowedExtension)) {
733
+ allowedExtensions.push(allowedExtension);
734
+ }
735
+ });
736
+
737
+ extensionsForMessage = allowedExtensions.join(', ').toLowerCase();
412
738
 
413
- r('{file}', this._formatFileName(fileName));
414
- r('{extensions}', extensions);
739
+ r('{file}', this._options.formatFileName(name));
740
+ r('{extensions}', extensionsForMessage);
415
741
  r('{sizeLimit}', this._formatSize(this._options.validation.sizeLimit));
416
742
  r('{minSizeLimit}', this._formatSize(this._options.validation.minSizeLimit));
417
743
 
418
- this._options.callbacks.onError(null, fileName, message);
744
+ this._options.callbacks.onError(null, name, message);
419
745
 
420
746
  return message;
421
747
  },
422
- _formatFileName: function(name){
423
- if (name.length > 33){
424
- name = name.slice(0, 19) + '...' + name.slice(-13);
425
- }
426
- return name;
748
+ _batchError: function(message) {
749
+ this._options.callbacks.onError(null, null, message);
427
750
  },
428
751
  _isAllowedExtension: function(fileName){
429
- var ext = (-1 !== fileName.indexOf('.'))
430
- ? fileName.replace(/.*[.]/, '').toLowerCase()
431
- : '';
432
- var allowed = this._options.validation.allowedExtensions;
433
-
434
- if (!allowed.length){return true;}
752
+ var allowed = this._options.validation.allowedExtensions,
753
+ valid = false;
435
754
 
436
- for (var i=0; i<allowed.length; i++){
437
- if (allowed[i].toLowerCase() == ext){ return true;}
755
+ if (!allowed.length) {
756
+ return true;
438
757
  }
439
758
 
440
- return false;
759
+ qq.each(allowed, function(idx, allowedExt) {
760
+ /**
761
+ * If an argument is not a string, ignore it. Added when a possible issue with MooTools hijacking the
762
+ * `allowedExtensions` array was discovered. See case #735 in the issue tracker for more details.
763
+ */
764
+ if (qq.isString(allowedExt)) {
765
+ /*jshint eqeqeq: true, eqnull: true*/
766
+ var extRegex = new RegExp('\\.' + allowedExt + "$", 'i');
767
+
768
+ if (fileName.match(extRegex) != null) {
769
+ valid = true;
770
+ return false;
771
+ }
772
+ }
773
+ });
774
+
775
+ return valid;
441
776
  },
442
777
  _formatSize: function(bytes){
443
778
  var i = -1;
@@ -446,7 +781,7 @@ qq.FineUploaderBasic.prototype = {
446
781
  i++;
447
782
  } while (bytes > 99);
448
783
 
449
- return Math.max(bytes, 0.1).toFixed(1) + ['kB', 'MB', 'GB', 'TB', 'PB', 'EB'][i];
784
+ return Math.max(bytes, 0.1).toFixed(1) + this._options.text.sizeSymbols[i];
450
785
  },
451
786
  _wrapCallbacks: function() {
452
787
  var self, safeCallback;
@@ -458,9 +793,9 @@ qq.FineUploaderBasic.prototype = {
458
793
  return callback.apply(self, args);
459
794
  }
460
795
  catch (exception) {
461
- self.log("Caught exception in '" + name + "' callback - " + exception, 'error');
796
+ self.log("Caught exception in '" + name + "' callback - " + exception.message, 'error');
462
797
  }
463
- }
798
+ };
464
799
 
465
800
  for (var prop in this._options.callbacks) {
466
801
  (function() {
@@ -469,40 +804,50 @@ qq.FineUploaderBasic.prototype = {
469
804
  callbackFunc = self._options.callbacks[callbackName];
470
805
  self._options.callbacks[callbackName] = function() {
471
806
  return safeCallback(callbackName, callbackFunc, arguments);
472
- }
807
+ };
473
808
  }());
474
809
  }
475
810
  },
476
- _parseFileName: function(file) {
811
+ _parseFileOrBlobDataName: function(fileOrBlobData) {
477
812
  var name;
478
813
 
479
- if (file.value){
480
- // it is a file input
481
- // get input value and remove path to normalize
482
- name = file.value.replace(/.*(\/|\\)/, "");
483
- } else {
484
- // fix missing properties in Safari 4 and firefox 11.0a2
485
- name = (file.fileName !== null && file.fileName !== undefined) ? file.fileName : file.name;
814
+ if (qq.isFileOrInput(fileOrBlobData)) {
815
+ if (fileOrBlobData.value) {
816
+ // it is a file input
817
+ // get input value and remove path to normalize
818
+ name = fileOrBlobData.value.replace(/.*(\/|\\)/, "");
819
+ } else {
820
+ // fix missing properties in Safari 4 and firefox 11.0a2
821
+ name = (fileOrBlobData.fileName !== null && fileOrBlobData.fileName !== undefined) ? fileOrBlobData.fileName : fileOrBlobData.name;
822
+ }
823
+ }
824
+ else {
825
+ name = fileOrBlobData.name;
486
826
  }
487
827
 
488
828
  return name;
489
829
  },
490
- _parseFileSize: function(file) {
830
+ _parseFileOrBlobDataSize: function(fileOrBlobData) {
491
831
  var size;
492
832
 
493
- if (!file.value){
494
- // fix missing properties in Safari 4 and firefox 11.0a2
495
- size = (file.fileSize !== null && file.fileSize !== undefined) ? file.fileSize : file.size;
833
+ if (qq.isFileOrInput(fileOrBlobData)) {
834
+ if (!fileOrBlobData.value){
835
+ // fix missing properties in Safari 4 and firefox 11.0a2
836
+ size = (fileOrBlobData.fileSize !== null && fileOrBlobData.fileSize !== undefined) ? fileOrBlobData.fileSize : fileOrBlobData.size;
837
+ }
838
+ }
839
+ else {
840
+ size = fileOrBlobData.blob.size;
496
841
  }
497
842
 
498
843
  return size;
499
844
  },
500
- _getValidationDescriptor: function(file) {
845
+ _getValidationDescriptor: function(fileOrBlobData) {
501
846
  var name, size, fileDescriptor;
502
847
 
503
848
  fileDescriptor = {};
504
- name = this._parseFileName(file);
505
- size = this._parseFileSize(file);
849
+ name = this._parseFileOrBlobDataName(fileOrBlobData);
850
+ size = this._parseFileOrBlobDataSize(fileOrBlobData);
506
851
 
507
852
  fileDescriptor.name = name;
508
853
  if (size) {
@@ -512,35 +857,35 @@ qq.FineUploaderBasic.prototype = {
512
857
  return fileDescriptor;
513
858
  },
514
859
  _getValidationDescriptors: function(files) {
515
- var index, fileDescriptors;
516
-
517
- fileDescriptors = [];
860
+ var self = this,
861
+ fileDescriptors = [];
518
862
 
519
- for (index = 0; index < files.length; index++) {
520
- fileDescriptors.push(files[index]);
521
- }
863
+ qq.each(files, function(idx, file) {
864
+ fileDescriptors.push(self._getValidationDescriptor(file));
865
+ });
522
866
 
523
867
  return fileDescriptors;
524
868
  },
525
- _createParamsStore: function() {
869
+ _createParamsStore: function(type) {
526
870
  var paramsStore = {},
527
871
  self = this;
528
872
 
529
873
  return {
530
- setParams: function(params, fileId) {
874
+ setParams: function(params, id) {
531
875
  var paramsCopy = {};
532
876
  qq.extend(paramsCopy, params);
533
- paramsStore[fileId] = paramsCopy;
877
+ paramsStore[id] = paramsCopy;
534
878
  },
535
879
 
536
- getParams: function(fileId) {
880
+ getParams: function(id) {
881
+ /*jshint eqeqeq: true, eqnull: true*/
537
882
  var paramsCopy = {};
538
883
 
539
- if (fileId !== undefined && paramsStore[fileId]) {
540
- qq.extend(paramsCopy, paramsStore[fileId]);
884
+ if (id != null && paramsStore[id]) {
885
+ qq.extend(paramsCopy, paramsStore[id]);
541
886
  }
542
887
  else {
543
- qq.extend(paramsCopy, self._options.request.params);
888
+ qq.extend(paramsCopy, self._options[type].params);
544
889
  }
545
890
 
546
891
  return paramsCopy;
@@ -553,6 +898,33 @@ qq.FineUploaderBasic.prototype = {
553
898
  reset: function() {
554
899
  paramsStore = {};
555
900
  }
556
- }
901
+ };
902
+ },
903
+ _createEndpointStore: function(type) {
904
+ var endpointStore = {},
905
+ self = this;
906
+
907
+ return {
908
+ setEndpoint: function(endpoint, id) {
909
+ endpointStore[id] = endpoint;
910
+ },
911
+
912
+ getEndpoint: function(id) {
913
+ /*jshint eqeqeq: true, eqnull: true*/
914
+ if (id != null && endpointStore[id]) {
915
+ return endpointStore[id];
916
+ }
917
+
918
+ return self._options[type].endpoint;
919
+ },
920
+
921
+ remove: function(fileId) {
922
+ return delete endpointStore[fileId];
923
+ },
924
+
925
+ reset: function() {
926
+ endpointStore = {};
927
+ }
928
+ };
557
929
  }
558
930
  };