fine_uploader 3.1.1 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  };