fineuploader-rails 3.0 → 3.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,5 @@
1
1
  module Fineuploader
2
2
  module Rails
3
- VERSION = "3.0"
3
+ VERSION = "3.3"
4
4
  end
5
5
  end
Binary file
@@ -1,6 +1,4 @@
1
1
  /**
2
- * Fine Uploader - version 3.0
3
- *
4
2
  * http://github.com/Valums-File-Uploader/file-uploader
5
3
  *
6
4
  * Multiple file upload component with progress-bar, drag-and-drop, support for all modern browsers.
@@ -10,8 +8,7 @@
10
8
  *
11
9
  * Licensed under MIT license, GNU GPL 2 or later, GNU LGPL 2 or later, see license.txt.
12
10
  */
13
-
14
- var qq = qq || {};
11
+ /*globals window, navigator, document, FormData, File, HTMLInputElement, XMLHttpRequest, Blob*/
15
12
  var qq = function(element) {
16
13
  "use strict";
17
14
 
@@ -44,13 +41,14 @@ var qq = function(element) {
44
41
 
45
42
  contains: function(descendant) {
46
43
  // compareposition returns false in this case
47
- if (element == descendant) {
44
+ if (element === descendant) {
48
45
  return true;
49
46
  }
50
47
 
51
48
  if (element.contains){
52
49
  return element.contains(descendant);
53
50
  } else {
51
+ /*jslint bitwise: true*/
54
52
  return !!(descendant.compareDocumentPosition(element) & 8);
55
53
  }
56
54
  },
@@ -73,8 +71,8 @@ var qq = function(element) {
73
71
  * Fixes opacity in IE6-8.
74
72
  */
75
73
  css: function(styles) {
76
- if (styles.opacity != null){
77
- if (typeof element.style.opacity != 'string' && typeof(element.filters) != 'undefined'){
74
+ if (styles.opacity !== null){
75
+ if (typeof element.style.opacity !== 'string' && typeof(element.filters) !== 'undefined'){
78
76
  styles.filter = 'alpha(opacity=' + Math.round(100 * styles.opacity) + ')';
79
77
  }
80
78
  }
@@ -102,19 +100,20 @@ var qq = function(element) {
102
100
  },
103
101
 
104
102
  getByClass: function(className) {
103
+ var candidates,
104
+ result = [];
105
+
105
106
  if (element.querySelectorAll){
106
107
  return element.querySelectorAll('.' + className);
107
108
  }
108
109
 
109
- var result = [];
110
- var candidates = element.getElementsByTagName("*");
111
- var len = candidates.length;
110
+ candidates = element.getElementsByTagName("*");
112
111
 
113
- for (var i = 0; i < len; i++){
114
- if (qq(candidates[i]).hasClass(className)){
115
- result.push(candidates[i]);
112
+ qq.each(candidates, function(idx, val) {
113
+ if (qq(val).hasClass(className)){
114
+ result.push(val);
116
115
  }
117
- }
116
+ });
118
117
  return result;
119
118
  },
120
119
 
@@ -123,7 +122,7 @@ var qq = function(element) {
123
122
  child = element.firstChild;
124
123
 
125
124
  while (child){
126
- if (child.nodeType == 1){
125
+ if (child.nodeType === 1){
127
126
  children.push(child);
128
127
  }
129
128
  child = child.nextSibling;
@@ -145,6 +144,8 @@ var qq = function(element) {
145
144
  };
146
145
 
147
146
  qq.log = function(message, level) {
147
+ "use strict";
148
+
148
149
  if (window.console) {
149
150
  if (!level || level === 'info') {
150
151
  window.console.log(message);
@@ -166,22 +167,84 @@ qq.isObject = function(variable) {
166
167
  return variable !== null && variable && typeof(variable) === "object" && variable.constructor === Object;
167
168
  };
168
169
 
169
- qq.extend = function (first, second, extendNested) {
170
+ qq.isFunction = function(variable) {
170
171
  "use strict";
171
- var prop;
172
- for (prop in second) {
173
- if (second.hasOwnProperty(prop)) {
174
- if (extendNested && qq.isObject(second[prop])) {
175
- if (first[prop] === undefined) {
176
- first[prop] = {};
177
- }
178
- qq.extend(first[prop], second[prop], true);
172
+ return typeof(variable) === "function";
173
+ };
174
+
175
+ qq.trimStr = function(string) {
176
+ if (String.prototype.trim) {
177
+ return string.trim();
178
+ }
179
+
180
+ return string.replace(/^\s+|\s+$/g,'');
181
+ };
182
+
183
+ qq.isFileOrInput = function(maybeFileOrInput) {
184
+ "use strict";
185
+ if (qq.isBlob(maybeFileOrInput) && window.File && maybeFileOrInput instanceof File) {
186
+ return true;
187
+ }
188
+ else if (window.HTMLInputElement) {
189
+ if (maybeFileOrInput instanceof HTMLInputElement) {
190
+ if (maybeFileOrInput.type && maybeFileOrInput.type.toLowerCase() === 'file') {
191
+ return true;
179
192
  }
180
- else {
181
- first[prop] = second[prop];
193
+ }
194
+ }
195
+ else if (maybeFileOrInput.tagName) {
196
+ if (maybeFileOrInput.tagName.toLowerCase() === 'input') {
197
+ if (maybeFileOrInput.type && maybeFileOrInput.type.toLowerCase() === 'file') {
198
+ return true;
182
199
  }
183
200
  }
184
201
  }
202
+
203
+ return false;
204
+ };
205
+
206
+ qq.isBlob = function(maybeBlob) {
207
+ "use strict";
208
+ return window.Blob && maybeBlob instanceof Blob;
209
+ };
210
+
211
+ qq.isXhrUploadSupported = function() {
212
+ "use strict";
213
+ var input = document.createElement('input');
214
+ input.type = 'file';
215
+
216
+ return (
217
+ input.multiple !== undefined &&
218
+ typeof File !== "undefined" &&
219
+ typeof FormData !== "undefined" &&
220
+ typeof (new XMLHttpRequest()).upload !== "undefined" );
221
+ };
222
+
223
+ qq.isFolderDropSupported = function(dataTransfer) {
224
+ "use strict";
225
+ return (dataTransfer.items && dataTransfer.items[0].webkitGetAsEntry);
226
+ };
227
+
228
+ qq.isFileChunkingSupported = function() {
229
+ "use strict";
230
+ return !qq.android() && //android's impl of Blob.slice is broken
231
+ qq.isXhrUploadSupported() &&
232
+ (File.prototype.slice || File.prototype.webkitSlice || File.prototype.mozSlice);
233
+ };
234
+
235
+ qq.extend = function (first, second, extendNested) {
236
+ "use strict";
237
+ qq.each(second, function(prop, val) {
238
+ if (extendNested && qq.isObject(val)) {
239
+ if (first[prop] === undefined) {
240
+ first[prop] = {};
241
+ }
242
+ qq.extend(first[prop], val, true);
243
+ }
244
+ else {
245
+ first[prop] = val;
246
+ }
247
+ });
185
248
  };
186
249
 
187
250
  /**
@@ -189,40 +252,75 @@ qq.extend = function (first, second, extendNested) {
189
252
  * @param {Number} [from] The index at which to begin the search
190
253
  */
191
254
  qq.indexOf = function(arr, elt, from){
192
- if (arr.indexOf) return arr.indexOf(elt, from);
255
+ "use strict";
256
+
257
+ if (arr.indexOf) {
258
+ return arr.indexOf(elt, from);
259
+ }
193
260
 
194
261
  from = from || 0;
195
262
  var len = arr.length;
196
263
 
197
- if (from < 0) from += len;
264
+ if (from < 0) {
265
+ from += len;
266
+ }
198
267
 
199
- for (; from < len; from++){
200
- if (from in arr && arr[from] === elt){
268
+ for (; from < len; from+=1){
269
+ if (arr.hasOwnProperty(from) && arr[from] === elt){
201
270
  return from;
202
271
  }
203
272
  }
204
273
  return -1;
205
274
  };
206
275
 
207
- qq.getUniqueId = (function(){
208
- var id = 0;
209
- return function(){ return id++; };
210
- })();
276
+ //this is a version 4 UUID
277
+ qq.getUniqueId = function(){
278
+ "use strict";
279
+
280
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
281
+ /*jslint eqeq: true, bitwise: true*/
282
+ var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
283
+ return v.toString(16);
284
+ });
285
+ };
211
286
 
212
287
  //
213
288
  // Browsers and platforms detection
214
289
 
215
- qq.ie = function(){ return navigator.userAgent.indexOf('MSIE') != -1; }
216
- qq.ie10 = function(){ return navigator.userAgent.indexOf('MSIE 10') != -1; }
217
- qq.safari = function(){ return navigator.vendor != undefined && navigator.vendor.indexOf("Apple") != -1; }
218
- qq.chrome = function(){ return navigator.vendor != undefined && navigator.vendor.indexOf('Google') != -1; }
219
- qq.firefox = function(){ return (navigator.userAgent.indexOf('Mozilla') != -1 && navigator.vendor != undefined && navigator.vendor == ''); }
220
- qq.windows = function(){ return navigator.platform == "Win32"; }
290
+ qq.ie = function(){
291
+ "use strict";
292
+ return navigator.userAgent.indexOf('MSIE') !== -1;
293
+ };
294
+ qq.ie10 = function(){
295
+ "use strict";
296
+ return navigator.userAgent.indexOf('MSIE 10') !== -1;
297
+ };
298
+ qq.safari = function(){
299
+ "use strict";
300
+ return navigator.vendor !== undefined && navigator.vendor.indexOf("Apple") !== -1;
301
+ };
302
+ qq.chrome = function(){
303
+ "use strict";
304
+ return navigator.vendor !== undefined && navigator.vendor.indexOf('Google') !== -1;
305
+ };
306
+ qq.firefox = function(){
307
+ "use strict";
308
+ return (navigator.userAgent.indexOf('Mozilla') !== -1 && navigator.vendor !== undefined && navigator.vendor === '');
309
+ };
310
+ qq.windows = function(){
311
+ "use strict";
312
+ return navigator.platform === "Win32";
313
+ };
314
+ qq.android = function(){
315
+ "use strict";
316
+ return navigator.userAgent.toLowerCase().indexOf('android') !== -1;
317
+ };
221
318
 
222
319
  //
223
320
  // Events
224
321
 
225
322
  qq.preventDefault = function(e){
323
+ "use strict";
226
324
  if (e.preventDefault){
227
325
  e.preventDefault();
228
326
  } else{
@@ -235,6 +333,7 @@ qq.preventDefault = function(e){
235
333
  * Uses innerHTML to create an element
236
334
  */
237
335
  qq.toElement = (function(){
336
+ "use strict";
238
337
  var div = document.createElement('div');
239
338
  return function(html){
240
339
  div.innerHTML = html;
@@ -242,7 +341,23 @@ qq.toElement = (function(){
242
341
  div.removeChild(element);
243
342
  return element;
244
343
  };
245
- })();
344
+ }());
345
+
346
+ //key and value are passed to callback for each item in the object or array
347
+ qq.each = function(obj, callback) {
348
+ "use strict";
349
+ var key, retVal;
350
+ if (obj) {
351
+ for (key in obj) {
352
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
353
+ retVal = callback(key, obj[key]);
354
+ if (retVal === false) {
355
+ break;
356
+ }
357
+ }
358
+ }
359
+ }
360
+ };
246
361
 
247
362
  /**
248
363
  * obj2url() takes a json-object as argument and generates
@@ -261,15 +376,18 @@ qq.toElement = (function(){
261
376
  * @return String encoded querystring
262
377
  */
263
378
  qq.obj2url = function(obj, temp, prefixDone){
264
- var uristrings = [],
265
- prefix = '&',
266
- add = function(nextObj, i){
379
+ "use strict";
380
+ /*jshint laxbreak: true*/
381
+ var i, len,
382
+ uristrings = [],
383
+ prefix = '&',
384
+ add = function(nextObj, i){
267
385
  var nextTemp = temp
268
386
  ? (/\[\]$/.test(temp)) // prevent double-encoding
269
387
  ? temp
270
388
  : temp+'['+i+']'
271
389
  : i;
272
- if ((nextTemp != 'undefined') && (i != 'undefined')) {
390
+ if ((nextTemp !== 'undefined') && (i !== 'undefined')) {
273
391
  uristrings.push(
274
392
  (typeof nextObj === 'object')
275
393
  ? qq.obj2url(nextObj, nextTemp, true)
@@ -284,15 +402,17 @@ qq.obj2url = function(obj, temp, prefixDone){
284
402
  prefix = (/\?/.test(temp)) ? (/\?$/.test(temp)) ? '' : '&' : '?';
285
403
  uristrings.push(temp);
286
404
  uristrings.push(qq.obj2url(obj));
287
- } else if ((Object.prototype.toString.call(obj) === '[object Array]') && (typeof obj != 'undefined') ) {
405
+ } else if ((Object.prototype.toString.call(obj) === '[object Array]') && (typeof obj !== 'undefined') ) {
288
406
  // we wont use a for-in-loop on an array (performance)
289
- for (var i = 0, len = obj.length; i < len; ++i){
407
+ for (i = -1, len = obj.length; i < len; i+=1){
290
408
  add(obj[i], i);
291
409
  }
292
- } else if ((typeof obj != 'undefined') && (obj !== null) && (typeof obj === "object")){
410
+ } else if ((typeof obj !== 'undefined') && (obj !== null) && (typeof obj === "object")){
293
411
  // for anything else but a scalar, we will use for-in-loop
294
- for (var i in obj){
295
- add(obj[i], i);
412
+ for (i in obj){
413
+ if (obj.hasOwnProperty(i)) {
414
+ add(obj[i], i);
415
+ }
296
416
  }
297
417
  } else {
298
418
  uristrings.push(encodeURIComponent(temp) + '=' + encodeURIComponent(obj));
@@ -307,29 +427,155 @@ qq.obj2url = function(obj, temp, prefixDone){
307
427
  }
308
428
  };
309
429
 
430
+ qq.obj2FormData = function(obj, formData, arrayKeyName) {
431
+ "use strict";
432
+ if (!formData) {
433
+ formData = new FormData();
434
+ }
435
+
436
+ qq.each(obj, function(key, val) {
437
+ key = arrayKeyName ? arrayKeyName + '[' + key + ']' : key;
438
+
439
+ if (qq.isObject(val)) {
440
+ qq.obj2FormData(val, formData, key);
441
+ }
442
+ else if (qq.isFunction(val)) {
443
+ formData.append(key, val());
444
+ }
445
+ else {
446
+ formData.append(key, val);
447
+ }
448
+ });
449
+
450
+ return formData;
451
+ };
452
+
453
+ qq.obj2Inputs = function(obj, form) {
454
+ "use strict";
455
+ var input;
456
+
457
+ if (!form) {
458
+ form = document.createElement('form');
459
+ }
460
+
461
+ qq.obj2FormData(obj, {
462
+ append: function(key, val) {
463
+ input = document.createElement('input');
464
+ input.setAttribute('name', key);
465
+ input.setAttribute('value', val);
466
+ form.appendChild(input);
467
+ }
468
+ });
469
+
470
+ return form;
471
+ };
472
+
473
+ qq.setCookie = function(name, value, days) {
474
+ var date = new Date(),
475
+ expires = "";
476
+
477
+ if (days) {
478
+ date.setTime(date.getTime()+(days*24*60*60*1000));
479
+ expires = "; expires="+date.toGMTString();
480
+ }
481
+
482
+ document.cookie = name+"="+value+expires+"; path=/";
483
+ };
484
+
485
+ qq.getCookie = function(name) {
486
+ var nameEQ = name + "=",
487
+ ca = document.cookie.split(';'),
488
+ c;
489
+
490
+ for(var i=0;i < ca.length;i++) {
491
+ c = ca[i];
492
+ while (c.charAt(0)==' ') {
493
+ c = c.substring(1,c.length);
494
+ }
495
+ if (c.indexOf(nameEQ) === 0) {
496
+ return c.substring(nameEQ.length,c.length);
497
+ }
498
+ }
499
+ };
500
+
501
+ qq.getCookieNames = function(regexp) {
502
+ var cookies = document.cookie.split(';'),
503
+ cookieNames = [];
504
+
505
+ qq.each(cookies, function(idx, cookie) {
506
+ cookie = qq.trimStr(cookie);
507
+
508
+ var equalsIdx = cookie.indexOf("=");
509
+
510
+ if (cookie.match(regexp)) {
511
+ cookieNames.push(cookie.substr(0, equalsIdx));
512
+ }
513
+ });
514
+
515
+ return cookieNames;
516
+ };
517
+
518
+ qq.deleteCookie = function(name) {
519
+ qq.setCookie(name, "", -1);
520
+ };
521
+
522
+ qq.areCookiesEnabled = function() {
523
+ var randNum = Math.random() * 100000,
524
+ name = "qqCookieTest:" + randNum;
525
+ qq.setCookie(name, 1);
526
+
527
+ if (qq.getCookie(name)) {
528
+ qq.deleteCookie(name);
529
+ return true;
530
+ }
531
+ return false;
532
+ };
533
+
534
+ /**
535
+ * Not recommended for use outside of Fine Uploader since this falls back to an unchecked eval if JSON.parse is not
536
+ * implemented. For a more secure JSON.parse polyfill, use Douglas Crockford's json2.js.
537
+ */
538
+ qq.parseJson = function(json) {
539
+ /*jshint evil: true*/
540
+ if (window.JSON && qq.isFunction(JSON.parse)) {
541
+ return JSON.parse(json);
542
+ } else {
543
+ return eval("(" + json + ")");
544
+ }
545
+ };
546
+
310
547
  /**
311
548
  * A generic module which supports object disposing in dispose() method.
312
549
  * */
313
- qq.DisposeSupport = {
314
- _disposers: [],
550
+ qq.DisposeSupport = function() {
551
+ "use strict";
552
+ var disposers = [];
315
553
 
316
- /** Run all registered disposers */
317
- dispose: function() {
318
- var disposer;
319
- while (disposer = this._disposers.shift()) {
320
- disposer();
321
- }
322
- },
554
+ return {
555
+ /** Run all registered disposers */
556
+ dispose: function() {
557
+ var disposer;
558
+ do {
559
+ disposer = disposers.shift();
560
+ if (disposer) {
561
+ disposer();
562
+ }
563
+ }
564
+ while (disposer);
565
+ },
323
566
 
324
- /** Add disposer to the collection */
325
- addDisposer: function(disposeFunction) {
326
- this._disposers.push(disposeFunction);
327
- },
567
+ /** Attach event handler and register de-attacher as a disposer */
568
+ attach: function() {
569
+ var args = arguments;
570
+ /*jslint undef:true*/
571
+ this.addDisposer(qq(args[0]).attach.apply(this, Array.prototype.slice.call(arguments, 1)));
572
+ },
328
573
 
329
- /** Attach event handler and register de-attacher as a disposer */
330
- _attach: function() {
331
- this.addDisposer(qq(arguments[0]).attach.apply(this, Array.prototype.slice.call(arguments, 1)));
332
- }
574
+ /** Add disposer to the collection */
575
+ addDisposer: function(disposeFunction) {
576
+ disposers.push(disposeFunction);
577
+ }
578
+ };
333
579
  };
334
580
  qq.UploadButton = function(o){
335
581
  this._options = {
@@ -345,7 +591,7 @@ qq.UploadButton = function(o){
345
591
  };
346
592
 
347
593
  qq.extend(this._options, o);
348
- qq.extend(this, qq.DisposeSupport);
594
+ this._disposeSupport = new qq.DisposeSupport();
349
595
 
350
596
  this._element = this._options.element;
351
597
 
@@ -406,20 +652,20 @@ qq.UploadButton.prototype = {
406
652
  this._element.appendChild(input);
407
653
 
408
654
  var self = this;
409
- this._attach(input, 'change', function(){
655
+ this._disposeSupport.attach(input, 'change', function(){
410
656
  self._options.onChange(input);
411
657
  });
412
658
 
413
- this._attach(input, 'mouseover', function(){
659
+ this._disposeSupport.attach(input, 'mouseover', function(){
414
660
  qq(self._element).addClass(self._options.hoverClass);
415
661
  });
416
- this._attach(input, 'mouseout', function(){
662
+ this._disposeSupport.attach(input, 'mouseout', function(){
417
663
  qq(self._element).removeClass(self._options.hoverClass);
418
664
  });
419
- this._attach(input, 'focus', function(){
665
+ this._disposeSupport.attach(input, 'focus', function(){
420
666
  qq(self._element).addClass(self._options.focusClass);
421
667
  });
422
- this._attach(input, 'blur', function(){
668
+ this._disposeSupport.attach(input, 'blur', function(){
423
669
  qq(self._element).removeClass(self._options.focusClass);
424
670
  });
425
671
 
@@ -445,9 +691,12 @@ qq.FineUploaderBasic = function(o){
445
691
  request: {
446
692
  endpoint: '/server/upload',
447
693
  params: {},
694
+ paramsInBody: true,
448
695
  customHeaders: {},
449
- forceMultipart: false,
450
- inputName: 'qqfile'
696
+ forceMultipart: true,
697
+ inputName: 'qqfile',
698
+ uuidName: 'qquuid',
699
+ totalFileSizeName: 'qqtotalfilesize'
451
700
  },
452
701
  validation: {
453
702
  allowedExtensions: [],
@@ -456,15 +705,21 @@ qq.FineUploaderBasic = function(o){
456
705
  stopOnFirstInvalidFile: true
457
706
  },
458
707
  callbacks: {
459
- onSubmit: function(id, fileName){}, // return false to cancel submit
460
- onComplete: function(id, fileName, responseJSON){},
461
- onCancel: function(id, fileName){},
462
- onUpload: function(id, fileName, xhr){},
463
- onProgress: function(id, fileName, loaded, total){},
464
- onError: function(id, fileName, reason) {},
465
- onAutoRetry: function(id, fileName, attemptNumber) {},
466
- onManualRetry: function(id, fileName) {},
467
- onValidate: function(fileData) {} // return false to prevent upload
708
+ onSubmit: function(id, name){},
709
+ onComplete: function(id, name, responseJSON){},
710
+ onCancel: function(id, name){},
711
+ onUpload: function(id, name){},
712
+ onUploadChunk: function(id, name, chunkData){},
713
+ onResume: function(id, fileName, chunkData){},
714
+ onProgress: function(id, name, loaded, total){},
715
+ onError: function(id, name, reason) {},
716
+ onAutoRetry: function(id, name, attemptNumber) {},
717
+ onManualRetry: function(id, name) {},
718
+ onValidateBatch: function(fileOrBlobData) {},
719
+ onValidate: function(fileOrBlobData) {},
720
+ onSubmitDelete: function(id) {},
721
+ onDelete: function(id){},
722
+ onDeleteComplete: function(id, xhr, isError){}
468
723
  },
469
724
  messages: {
470
725
  typeError: "{file} has an invalid extension. Valid extension(s): {extensions}.",
@@ -479,23 +734,79 @@ qq.FineUploaderBasic = function(o){
479
734
  maxAutoAttempts: 3,
480
735
  autoAttemptDelay: 5,
481
736
  preventRetryResponseProperty: 'preventRetry'
737
+ },
738
+ classes: {
739
+ buttonHover: 'qq-upload-button-hover',
740
+ buttonFocus: 'qq-upload-button-focus'
741
+ },
742
+ chunking: {
743
+ enabled: false,
744
+ partSize: 2000000,
745
+ paramNames: {
746
+ partIndex: 'qqpartindex',
747
+ partByteOffset: 'qqpartbyteoffset',
748
+ chunkSize: 'qqchunksize',
749
+ totalFileSize: 'qqtotalfilesize',
750
+ totalParts: 'qqtotalparts',
751
+ filename: 'qqfilename'
752
+ }
753
+ },
754
+ resume: {
755
+ enabled: false,
756
+ id: null,
757
+ cookiesExpireIn: 7, //days
758
+ paramNames: {
759
+ resuming: "qqresume"
760
+ }
761
+ },
762
+ formatFileName: function(fileOrBlobName) {
763
+ if (fileOrBlobName.length > 33) {
764
+ fileOrBlobName = fileOrBlobName.slice(0, 19) + '...' + fileOrBlobName.slice(-14);
765
+ }
766
+ return fileOrBlobName;
767
+ },
768
+ text: {
769
+ sizeSymbols: ['kB', 'MB', 'GB', 'TB', 'PB', 'EB']
770
+ },
771
+ deleteFile : {
772
+ enabled: false,
773
+ endpoint: '/server/upload',
774
+ customHeaders: {},
775
+ params: {}
776
+ },
777
+ cors: {
778
+ expected: false,
779
+ sendCredentials: false
780
+ },
781
+ blobs: {
782
+ defaultName: 'Misc data',
783
+ paramNames: {
784
+ name: 'qqblobname'
785
+ }
482
786
  }
483
787
  };
484
788
 
485
789
  qq.extend(this._options, o, true);
486
790
  this._wrapCallbacks();
487
- qq.extend(this, qq.DisposeSupport);
791
+ this._disposeSupport = new qq.DisposeSupport();
488
792
 
489
793
  // number of files being uploaded
490
- this._filesInProgress = 0;
794
+ this._filesInProgress = [];
491
795
 
492
- this._storedFileIds = [];
796
+ this._storedIds = [];
493
797
 
494
798
  this._autoRetries = [];
495
799
  this._retryTimeouts = [];
496
800
  this._preventRetries = [];
497
801
 
802
+ this._paramsStore = this._createParamsStore("request");
803
+ this._deleteFileParamsStore = this._createParamsStore("deleteFile");
804
+
805
+ this._endpointStore = this._createEndpointStore("request");
806
+ this._deleteFileEndpointStore = this._createEndpointStore("deleteFile");
807
+
498
808
  this._handler = this._createUploadHandler();
809
+ this._deleteHandler = this._createDeleteHandler();
499
810
 
500
811
  if (this._options.button){
501
812
  this._button = this._createUploadButton(this._options.button);
@@ -514,21 +825,48 @@ qq.FineUploaderBasic.prototype = {
514
825
 
515
826
  }
516
827
  },
517
- setParams: function(params){
518
- this._options.request.params = params;
828
+ setParams: function(params, id) {
829
+ /*jshint eqeqeq: true, eqnull: true*/
830
+ if (id == null) {
831
+ this._options.request.params = params;
832
+ }
833
+ else {
834
+ this._paramsStore.setParams(params, id);
835
+ }
836
+ },
837
+ setDeleteFileParams: function(params, id) {
838
+ /*jshint eqeqeq: true, eqnull: true*/
839
+ if (id == null) {
840
+ this._options.deleteFile.params = params;
841
+ }
842
+ else {
843
+ this._deleteFileParamsStore.setParams(params, id);
844
+ }
845
+ },
846
+ setEndpoint: function(endpoint, id) {
847
+ /*jshint eqeqeq: true, eqnull: true*/
848
+ if (id == null) {
849
+ this._options.request.endpoint = endpoint;
850
+ }
851
+ else {
852
+ this._endpointStore.setEndpoint(endpoint, id);
853
+ }
519
854
  },
520
855
  getInProgress: function(){
521
- return this._filesInProgress;
856
+ return this._filesInProgress.length;
522
857
  },
523
858
  uploadStoredFiles: function(){
524
859
  "use strict";
525
- while(this._storedFileIds.length) {
526
- this._filesInProgress++;
527
- this._handler.upload(this._storedFileIds.shift(), this._options.request.params);
860
+ var idToUpload;
861
+
862
+ while(this._storedIds.length) {
863
+ idToUpload = this._storedIds.shift();
864
+ this._filesInProgress.push(idToUpload);
865
+ this._handler.upload(idToUpload);
528
866
  }
529
867
  },
530
868
  clearStoredFiles: function(){
531
- this._storedFileIds = [];
869
+ this._storedIds = [];
532
870
  },
533
871
  retry: function(id) {
534
872
  if (this._onBeforeManualRetry(id)) {
@@ -539,79 +877,179 @@ qq.FineUploaderBasic.prototype = {
539
877
  return false;
540
878
  }
541
879
  },
542
- cancel: function(fileId) {
543
- this._handler.cancel(fileId);
880
+ cancel: function(id) {
881
+ this._handler.cancel(id);
882
+ },
883
+ cancelAll: function() {
884
+ var storedIdsCopy = [],
885
+ self = this;
886
+
887
+ qq.extend(storedIdsCopy, this._storedIds);
888
+ qq.each(storedIdsCopy, function(idx, storedFileId) {
889
+ self.cancel(storedFileId);
890
+ });
891
+
892
+ this._handler.cancelAll();
544
893
  },
545
894
  reset: function() {
546
895
  this.log("Resetting uploader...");
547
896
  this._handler.reset();
548
- this._filesInProgress = 0;
549
- this._storedFileIds = [];
897
+ this._filesInProgress = [];
898
+ this._storedIds = [];
550
899
  this._autoRetries = [];
551
900
  this._retryTimeouts = [];
552
901
  this._preventRetries = [];
553
902
  this._button.reset();
903
+ this._paramsStore.reset();
904
+ this._endpointStore.reset();
905
+ },
906
+ addFiles: function(filesBlobDataOrInputs) {
907
+ var self = this,
908
+ verifiedFilesOrInputs = [],
909
+ index, fileOrInput;
910
+
911
+ if (filesBlobDataOrInputs) {
912
+ if (!window.FileList || !(filesBlobDataOrInputs instanceof FileList)) {
913
+ filesBlobDataOrInputs = [].concat(filesBlobDataOrInputs);
914
+ }
915
+
916
+ for (index = 0; index < filesBlobDataOrInputs.length; index+=1) {
917
+ fileOrInput = filesBlobDataOrInputs[index];
918
+
919
+ if (qq.isFileOrInput(fileOrInput)) {
920
+ verifiedFilesOrInputs.push(fileOrInput);
921
+ }
922
+ else {
923
+ self.log(fileOrInput + ' is not a File or INPUT element! Ignoring!', 'warn');
924
+ }
925
+ }
926
+
927
+ this.log('Processing ' + verifiedFilesOrInputs.length + ' files or inputs...');
928
+ this._uploadFileOrBlobDataList(verifiedFilesOrInputs);
929
+ }
930
+ },
931
+ addBlobs: function(blobDataOrArray) {
932
+ if (blobDataOrArray) {
933
+ var blobDataArray = [].concat(blobDataOrArray),
934
+ verifiedBlobDataList = [],
935
+ self = this;
936
+
937
+ qq.each(blobDataArray, function(idx, blobData) {
938
+ if (qq.isBlob(blobData) && !qq.isFileOrInput(blobData)) {
939
+ verifiedBlobDataList.push({
940
+ blob: blobData,
941
+ name: self._options.blobs.defaultName
942
+ });
943
+ }
944
+ else if (qq.isObject(blobData) && blobData.blob && blobData.name) {
945
+ verifiedBlobDataList.push(blobData);
946
+ }
947
+ else {
948
+ self.log("addBlobs: entry at index " + idx + " is not a Blob or a BlobData object", "error");
949
+ }
950
+ });
951
+
952
+ this._uploadFileOrBlobDataList(verifiedBlobDataList);
953
+ }
954
+ else {
955
+ this.log("undefined or non-array parameter passed into addBlobs", "error");
956
+ }
957
+ },
958
+ getUuid: function(id) {
959
+ return this._handler.getUuid(id);
960
+ },
961
+ getResumableFilesData: function() {
962
+ return this._handler.getResumableFilesData();
963
+ },
964
+ getSize: function(id) {
965
+ return this._handler.getSize(id);
966
+ },
967
+ getFile: function(fileOrBlobId) {
968
+ return this._handler.getFile(fileOrBlobId);
969
+ },
970
+ deleteFile: function(id) {
971
+ this._onSubmitDelete(id);
972
+ },
973
+ setDeleteFileEndpoint: function(endpoint, id) {
974
+ /*jshint eqeqeq: true, eqnull: true*/
975
+ if (id == null) {
976
+ this._options.deleteFile.endpoint = endpoint;
977
+ }
978
+ else {
979
+ this._deleteFileEndpointStore.setEndpoint(endpoint, id);
980
+ }
554
981
  },
555
982
  _createUploadButton: function(element){
556
983
  var self = this;
557
984
 
558
985
  var button = new qq.UploadButton({
559
986
  element: element,
560
- multiple: this._options.multiple && qq.UploadHandlerXhr.isSupported(),
987
+ multiple: this._options.multiple && qq.isXhrUploadSupported(),
561
988
  acceptFiles: this._options.validation.acceptFiles,
562
989
  onChange: function(input){
563
990
  self._onInputChange(input);
564
- }
991
+ },
992
+ hoverClass: this._options.classes.buttonHover,
993
+ focusClass: this._options.classes.buttonFocus
565
994
  });
566
995
 
567
- this.addDisposer(function() { button.dispose(); });
996
+ this._disposeSupport.addDisposer(function() { button.dispose(); });
568
997
  return button;
569
998
  },
570
999
  _createUploadHandler: function(){
571
- var self = this,
572
- handlerClass;
573
-
574
- if(qq.UploadHandlerXhr.isSupported()){
575
- handlerClass = 'UploadHandlerXhr';
576
- } else {
577
- handlerClass = 'UploadHandlerForm';
578
- }
1000
+ var self = this;
579
1001
 
580
- var handler = new qq[handlerClass]({
1002
+ return new qq.UploadHandler({
581
1003
  debug: this._options.debug,
582
- endpoint: this._options.request.endpoint,
583
1004
  forceMultipart: this._options.request.forceMultipart,
584
1005
  maxConnections: this._options.maxConnections,
585
1006
  customHeaders: this._options.request.customHeaders,
586
1007
  inputName: this._options.request.inputName,
1008
+ uuidParamName: this._options.request.uuidName,
1009
+ totalFileSizeParamName: this._options.request.totalFileSizeName,
1010
+ cors: this._options.cors,
587
1011
  demoMode: this._options.demoMode,
588
- log: this.log,
589
- onProgress: function(id, fileName, loaded, total){
590
- self._onProgress(id, fileName, loaded, total);
591
- self._options.callbacks.onProgress(id, fileName, loaded, total);
1012
+ paramsInBody: this._options.request.paramsInBody,
1013
+ paramsStore: this._paramsStore,
1014
+ endpointStore: this._endpointStore,
1015
+ chunking: this._options.chunking,
1016
+ resume: this._options.resume,
1017
+ blobs: this._options.blobs,
1018
+ log: function(str, level) {
1019
+ self.log(str, level);
1020
+ },
1021
+ onProgress: function(id, name, loaded, total){
1022
+ self._onProgress(id, name, loaded, total);
1023
+ self._options.callbacks.onProgress(id, name, loaded, total);
592
1024
  },
593
- onComplete: function(id, fileName, result, xhr){
594
- self._onComplete(id, fileName, result, xhr);
595
- self._options.callbacks.onComplete(id, fileName, result);
1025
+ onComplete: function(id, name, result, xhr){
1026
+ self._onComplete(id, name, result, xhr);
1027
+ self._options.callbacks.onComplete(id, name, result);
596
1028
  },
597
- onCancel: function(id, fileName){
598
- self._onCancel(id, fileName);
599
- self._options.callbacks.onCancel(id, fileName);
1029
+ onCancel: function(id, name){
1030
+ self._onCancel(id, name);
1031
+ self._options.callbacks.onCancel(id, name);
600
1032
  },
601
- onUpload: function(id, fileName, xhr){
602
- self._onUpload(id, fileName, xhr);
603
- self._options.callbacks.onUpload(id, fileName, xhr);
1033
+ onUpload: function(id, name){
1034
+ self._onUpload(id, name);
1035
+ self._options.callbacks.onUpload(id, name);
604
1036
  },
605
- onAutoRetry: function(id, fileName, responseJSON, xhr) {
1037
+ onUploadChunk: function(id, name, chunkData){
1038
+ self._options.callbacks.onUploadChunk(id, name, chunkData);
1039
+ },
1040
+ onResume: function(id, name, chunkData) {
1041
+ return self._options.callbacks.onResume(id, name, chunkData);
1042
+ },
1043
+ onAutoRetry: function(id, name, responseJSON, xhr) {
606
1044
  self._preventRetries[id] = responseJSON[self._options.retry.preventRetryResponseProperty];
607
1045
 
608
- if (self._shouldAutoRetry(id, fileName, responseJSON)) {
609
- self._maybeParseAndSendUploadError(id, fileName, responseJSON, xhr);
610
- self._options.callbacks.onAutoRetry(id, fileName, self._autoRetries[id] + 1);
611
- self._onBeforeAutoRetry(id, fileName);
1046
+ if (self._shouldAutoRetry(id, name, responseJSON)) {
1047
+ self._maybeParseAndSendUploadError(id, name, responseJSON, xhr);
1048
+ self._options.callbacks.onAutoRetry(id, name, self._autoRetries[id] + 1);
1049
+ self._onBeforeAutoRetry(id, name);
612
1050
 
613
1051
  self._retryTimeouts[id] = setTimeout(function() {
614
- self._onAutoRetry(id, fileName, responseJSON)
1052
+ self._onAutoRetry(id, name, responseJSON)
615
1053
  }, self._options.retry.autoAttemptDelay * 1000);
616
1054
 
617
1055
  return true;
@@ -621,14 +1059,36 @@ qq.FineUploaderBasic.prototype = {
621
1059
  }
622
1060
  }
623
1061
  });
1062
+ },
1063
+ _createDeleteHandler: function() {
1064
+ var self = this;
624
1065
 
625
- return handler;
1066
+ return new qq.DeleteFileAjaxRequestor({
1067
+ maxConnections: this._options.maxConnections,
1068
+ customHeaders: this._options.deleteFile.customHeaders,
1069
+ paramsStore: this._deleteFileParamsStore,
1070
+ endpointStore: this._deleteFileEndpointStore,
1071
+ demoMode: this._options.demoMode,
1072
+ cors: this._options.cors,
1073
+ log: function(str, level) {
1074
+ self.log(str, level);
1075
+ },
1076
+ onDelete: function(id) {
1077
+ self._onDelete(id);
1078
+ self._options.callbacks.onDelete(id);
1079
+ },
1080
+ onDeleteComplete: function(id, xhr, isError) {
1081
+ self._onDeleteComplete(id, xhr, isError);
1082
+ self._options.callbacks.onDeleteComplete(id, xhr, isError);
1083
+ }
1084
+
1085
+ });
626
1086
  },
627
1087
  _preventLeaveInProgress: function(){
628
1088
  var self = this;
629
1089
 
630
- this._attach(window, 'beforeunload', function(e){
631
- if (!self._filesInProgress){return;}
1090
+ this._disposeSupport.attach(window, 'beforeunload', function(e){
1091
+ if (!self._filesInProgress.length){return;}
632
1092
 
633
1093
  var e = e || window.event;
634
1094
  // for ie, ff
@@ -637,49 +1097,82 @@ qq.FineUploaderBasic.prototype = {
637
1097
  return self._options.messages.onLeave;
638
1098
  });
639
1099
  },
640
- _onSubmit: function(id, fileName){
1100
+ _onSubmit: function(id, name){
641
1101
  if (this._options.autoUpload) {
642
- this._filesInProgress++;
1102
+ this._filesInProgress.push(id);
643
1103
  }
644
1104
  },
645
- _onProgress: function(id, fileName, loaded, total){
1105
+ _onProgress: function(id, name, loaded, total){
646
1106
  },
647
- _onComplete: function(id, fileName, result, xhr){
648
- this._filesInProgress--;
649
- this._maybeParseAndSendUploadError(id, fileName, result, xhr);
1107
+ _onComplete: function(id, name, result, xhr){
1108
+ this._removeFromFilesInProgress(id);
1109
+ this._maybeParseAndSendUploadError(id, name, result, xhr);
650
1110
  },
651
- _onCancel: function(id, fileName){
1111
+ _onCancel: function(id, name){
1112
+ this._removeFromFilesInProgress(id);
1113
+
652
1114
  clearTimeout(this._retryTimeouts[id]);
653
1115
 
654
- var storedFileIndex = qq.indexOf(this._storedFileIds, id);
655
- if (this._options.autoUpload || storedFileIndex < 0) {
656
- this._filesInProgress--;
1116
+ var storedItemIndex = qq.indexOf(this._storedIds, id);
1117
+ if (!this._options.autoUpload && storedItemIndex >= 0) {
1118
+ this._storedIds.splice(storedItemIndex, 1);
1119
+ }
1120
+ },
1121
+ _isDeletePossible: function() {
1122
+ return (this._options.deleteFile.enabled &&
1123
+ (!this._options.cors.expected ||
1124
+ (this._options.cors.expected && (qq.ie10() || !qq.ie()))
1125
+ )
1126
+ );
1127
+ },
1128
+ _onSubmitDelete: function(id) {
1129
+ if (this._isDeletePossible()) {
1130
+ if (this._options.callbacks.onSubmitDelete(id)) {
1131
+ this._deleteHandler.sendDelete(id, this.getUuid(id));
1132
+ }
1133
+ }
1134
+ else {
1135
+ this.log("Delete request ignored for ID " + id + ", delete feature is disabled or request not possible " +
1136
+ "due to CORS on a user agent that does not support pre-flighting.", "warn");
1137
+ return false;
1138
+ }
1139
+ },
1140
+ _onDelete: function(fileId) {},
1141
+ _onDeleteComplete: function(id, xhr, isError) {
1142
+ var name = this._handler.getName(id);
1143
+
1144
+ if (isError) {
1145
+ this.log("Delete request for '" + name + "' has failed.", "error");
1146
+ this._options.callbacks.onError(id, name, "Delete request failed with response code " + xhr.status);
657
1147
  }
658
- else if (!this._options.autoUpload) {
659
- this._storedFileIds.splice(storedFileIndex, 1);
1148
+ else {
1149
+ this.log("Delete request for '" + name + "' has succeeded.");
660
1150
  }
661
1151
  },
662
- _onUpload: function(id, fileName, xhr){
1152
+ _removeFromFilesInProgress: function(id) {
1153
+ var index = qq.indexOf(this._filesInProgress, id);
1154
+ if (index >= 0) {
1155
+ this._filesInProgress.splice(index, 1);
1156
+ }
663
1157
  },
1158
+ _onUpload: function(id, name){},
664
1159
  _onInputChange: function(input){
665
- if (this._handler instanceof qq.UploadHandlerXhr){
666
- this._uploadFileList(input.files);
1160
+ if (qq.isXhrUploadSupported()){
1161
+ this.addFiles(input.files);
667
1162
  } else {
668
- if (this._validateFile(input)){
669
- this._uploadFile(input);
670
- }
1163
+ this.addFiles(input);
671
1164
  }
672
1165
  this._button.reset();
673
1166
  },
674
- _onBeforeAutoRetry: function(id, fileName) {
675
- this.log("Waiting " + this._options.retry.autoAttemptDelay + " seconds before retrying " + fileName + "...");
1167
+ _onBeforeAutoRetry: function(id, name) {
1168
+ this.log("Waiting " + this._options.retry.autoAttemptDelay + " seconds before retrying " + name + "...");
676
1169
  },
677
- _onAutoRetry: function(id, fileName, responseJSON) {
678
- this.log("Retrying " + fileName + "...");
1170
+ _onAutoRetry: function(id, name, responseJSON) {
1171
+ this.log("Retrying " + name + "...");
679
1172
  this._autoRetries[id]++;
680
1173
  this._handler.retry(id);
681
1174
  },
682
- _shouldAutoRetry: function(id, fileName, responseJSON) {
1175
+ _shouldAutoRetry: function(id, name, responseJSON) {
683
1176
  if (!this._preventRetries[id] && this._options.retry.enableAuto) {
684
1177
  if (this._autoRetries[id] === undefined) {
685
1178
  this._autoRetries[id] = 0;
@@ -704,7 +1197,7 @@ qq.FineUploaderBasic.prototype = {
704
1197
  }
705
1198
 
706
1199
  this.log("Retrying upload for '" + fileName + "' (id: " + id + ")...");
707
- this._filesInProgress++;
1200
+ this._filesInProgress.push(id);
708
1201
  return true;
709
1202
  }
710
1203
  else {
@@ -712,31 +1205,29 @@ qq.FineUploaderBasic.prototype = {
712
1205
  return false;
713
1206
  }
714
1207
  },
715
- _maybeParseAndSendUploadError: function(id, fileName, response, xhr) {
1208
+ _maybeParseAndSendUploadError: function(id, name, response, xhr) {
716
1209
  //assuming no one will actually set the response code to something other than 200 and still set 'success' to true
717
1210
  if (!response.success){
718
1211
  if (xhr && xhr.status !== 200 && !response.error) {
719
- this._options.callbacks.onError(id, fileName, "XHR returned response code " + xhr.status);
1212
+ this._options.callbacks.onError(id, name, "XHR returned response code " + xhr.status);
720
1213
  }
721
1214
  else {
722
1215
  var errorReason = response.error ? response.error : "Upload failure reason unknown";
723
- this._options.callbacks.onError(id, fileName, errorReason);
1216
+ this._options.callbacks.onError(id, name, errorReason);
724
1217
  }
725
1218
  }
726
1219
  },
727
- _uploadFileList: function(files){
1220
+ _uploadFileOrBlobDataList: function(fileOrBlobDataList){
728
1221
  var validationDescriptors, index, batchInvalid;
729
1222
 
730
- validationDescriptors = this._getValidationDescriptors(files);
731
- if (validationDescriptors.length > 1) {
732
- batchInvalid = this._options.callbacks.onValidate(validationDescriptors) === false;
733
- }
1223
+ validationDescriptors = this._getValidationDescriptors(fileOrBlobDataList);
1224
+ batchInvalid = this._options.callbacks.onValidateBatch(validationDescriptors) === false;
734
1225
 
735
1226
  if (!batchInvalid) {
736
- if (files.length > 0) {
737
- for (index = 0; index < files.length; index++){
738
- if (this._validateFile(files[index])){
739
- this._uploadFile(files[index]);
1227
+ if (fileOrBlobDataList.length > 0) {
1228
+ for (index = 0; index < fileOrBlobDataList.length; index++){
1229
+ if (this._validateFileOrBlobData(fileOrBlobDataList[index])){
1230
+ this._upload(fileOrBlobDataList[index]);
740
1231
  } else {
741
1232
  if (this._options.validation.stopOnFirstInvalidFile){
742
1233
  return;
@@ -749,35 +1240,35 @@ qq.FineUploaderBasic.prototype = {
749
1240
  }
750
1241
  }
751
1242
  },
752
- _uploadFile: function(fileContainer){
753
- var id = this._handler.add(fileContainer);
754
- var fileName = this._handler.getName(id);
1243
+ _upload: function(blobOrFileContainer){
1244
+ var id = this._handler.add(blobOrFileContainer);
1245
+ var name = this._handler.getName(id);
755
1246
 
756
- if (this._options.callbacks.onSubmit(id, fileName) !== false){
757
- this._onSubmit(id, fileName);
1247
+ if (this._options.callbacks.onSubmit(id, name) !== false){
1248
+ this._onSubmit(id, name);
758
1249
  if (this._options.autoUpload) {
759
- this._handler.upload(id, this._options.request.params);
1250
+ this._handler.upload(id);
760
1251
  }
761
1252
  else {
762
- this._storeFileForLater(id);
1253
+ this._storeForLater(id);
763
1254
  }
764
1255
  }
765
1256
  },
766
- _storeFileForLater: function(id) {
767
- this._storedFileIds.push(id);
1257
+ _storeForLater: function(id) {
1258
+ this._storedIds.push(id);
768
1259
  },
769
- _validateFile: function(file){
1260
+ _validateFileOrBlobData: function(fileOrBlobData){
770
1261
  var validationDescriptor, name, size;
771
1262
 
772
- validationDescriptor = this._getValidationDescriptor(file);
1263
+ validationDescriptor = this._getValidationDescriptor(fileOrBlobData);
773
1264
  name = validationDescriptor.name;
774
1265
  size = validationDescriptor.size;
775
1266
 
776
- if (this._options.callbacks.onValidate([validationDescriptor]) === false) {
1267
+ if (this._options.callbacks.onValidate(validationDescriptor) === false) {
777
1268
  return false;
778
1269
  }
779
1270
 
780
- if (!this._isAllowedExtension(name)){
1271
+ if (qq.isFileOrInput(fileOrBlobData) && !this._isAllowedExtension(name)){
781
1272
  this._error('typeError', name);
782
1273
  return false;
783
1274
 
@@ -799,49 +1290,49 @@ qq.FineUploaderBasic.prototype = {
799
1290
 
800
1291
  return true;
801
1292
  },
802
- _error: function(code, fileName){
1293
+ _error: function(code, name){
803
1294
  var message = this._options.messages[code];
804
1295
  function r(name, replacement){ message = message.replace(name, replacement); }
805
1296
 
806
- var extensions = this._options.validation.allowedExtensions.join(', ');
1297
+ var extensions = this._options.validation.allowedExtensions.join(', ').toLowerCase();
807
1298
 
808
- r('{file}', this._formatFileName(fileName));
1299
+ r('{file}', this._options.formatFileName(name));
809
1300
  r('{extensions}', extensions);
810
1301
  r('{sizeLimit}', this._formatSize(this._options.validation.sizeLimit));
811
1302
  r('{minSizeLimit}', this._formatSize(this._options.validation.minSizeLimit));
812
1303
 
813
- this._options.callbacks.onError(null, fileName, message);
1304
+ this._options.callbacks.onError(null, name, message);
814
1305
 
815
1306
  return message;
816
1307
  },
817
- _formatFileName: function(name){
818
- if (name.length > 33){
819
- name = name.slice(0, 19) + '...' + name.slice(-13);
820
- }
821
- return name;
822
- },
823
1308
  _isAllowedExtension: function(fileName){
824
- var ext = (-1 !== fileName.indexOf('.'))
825
- ? fileName.replace(/.*[.]/, '').toLowerCase()
826
- : '';
827
- var allowed = this._options.validation.allowedExtensions;
828
-
829
- if (!allowed.length){return true;}
1309
+ var allowed = this._options.validation.allowedExtensions,
1310
+ valid = false;
830
1311
 
831
- for (var i=0; i<allowed.length; i++){
832
- if (allowed[i].toLowerCase() == ext){ return true;}
1312
+ if (!allowed.length) {
1313
+ return true;
833
1314
  }
834
1315
 
835
- return false;
836
- },
837
- _formatSize: function(bytes){
838
- var i = -1;
839
- do {
1316
+ qq.each(allowed, function(idx, allowedExt) {
1317
+ /*jshint eqeqeq: true, eqnull: true*/
1318
+ var extRegex = new RegExp('\\.' + allowedExt + "$", 'i');
1319
+
1320
+ if (fileName.match(extRegex) != null) {
1321
+ valid = true;
1322
+ return false;
1323
+ }
1324
+ });
1325
+
1326
+ return valid;
1327
+ },
1328
+ _formatSize: function(bytes){
1329
+ var i = -1;
1330
+ do {
840
1331
  bytes = bytes / 1024;
841
1332
  i++;
842
1333
  } while (bytes > 99);
843
1334
 
844
- return Math.max(bytes, 0.1).toFixed(1) + ['kB', 'MB', 'GB', 'TB', 'PB', 'EB'][i];
1335
+ return Math.max(bytes, 0.1).toFixed(1) + this._options.text.sizeSymbols[i];
845
1336
  },
846
1337
  _wrapCallbacks: function() {
847
1338
  var self, safeCallback;
@@ -853,49 +1344,61 @@ qq.FineUploaderBasic.prototype = {
853
1344
  return callback.apply(self, args);
854
1345
  }
855
1346
  catch (exception) {
856
- self.log("Caught exception in '" + name + "' callback - " + exception, 'error');
1347
+ self.log("Caught exception in '" + name + "' callback - " + exception.message, 'error');
857
1348
  }
858
1349
  }
859
1350
 
860
1351
  for (var prop in this._options.callbacks) {
861
1352
  (function() {
862
- var oldCallback = self._options.callbacks[prop];
863
- self._options.callbacks[prop] = function() {
864
- return safeCallback(prop, oldCallback, arguments);
1353
+ var callbackName, callbackFunc;
1354
+ callbackName = prop;
1355
+ callbackFunc = self._options.callbacks[callbackName];
1356
+ self._options.callbacks[callbackName] = function() {
1357
+ return safeCallback(callbackName, callbackFunc, arguments);
865
1358
  }
866
1359
  }());
867
1360
  }
868
1361
  },
869
- _parseFileName: function(file) {
1362
+ _parseFileOrBlobDataName: function(fileOrBlobData) {
870
1363
  var name;
871
1364
 
872
- if (file.value){
873
- // it is a file input
874
- // get input value and remove path to normalize
875
- name = file.value.replace(/.*(\/|\\)/, "");
876
- } else {
877
- // fix missing properties in Safari 4 and firefox 11.0a2
878
- name = (file.fileName !== null && file.fileName !== undefined) ? file.fileName : file.name;
1365
+ if (qq.isFileOrInput(fileOrBlobData)) {
1366
+ if (fileOrBlobData.value) {
1367
+ // it is a file input
1368
+ // get input value and remove path to normalize
1369
+ name = fileOrBlobData.value.replace(/.*(\/|\\)/, "");
1370
+ } else {
1371
+ // fix missing properties in Safari 4 and firefox 11.0a2
1372
+ name = (fileOrBlobData.fileName !== null && fileOrBlobData.fileName !== undefined) ? fileOrBlobData.fileName : fileOrBlobData.name;
1373
+ }
1374
+ }
1375
+ else {
1376
+ name = fileOrBlobData.name;
879
1377
  }
880
1378
 
881
1379
  return name;
882
1380
  },
883
- _parseFileSize: function(file) {
1381
+ _parseFileOrBlobDataSize: function(fileOrBlobData) {
884
1382
  var size;
885
1383
 
886
- if (!file.value){
887
- // fix missing properties in Safari 4 and firefox 11.0a2
888
- size = (file.fileSize !== null && file.fileSize !== undefined) ? file.fileSize : file.size;
1384
+ if (qq.isFileOrInput(fileOrBlobData)) {
1385
+ if (!fileOrBlobData.value){
1386
+ // fix missing properties in Safari 4 and firefox 11.0a2
1387
+ size = (fileOrBlobData.fileSize !== null && fileOrBlobData.fileSize !== undefined) ? fileOrBlobData.fileSize : fileOrBlobData.size;
1388
+ }
1389
+ }
1390
+ else {
1391
+ size = fileOrBlobData.blob.size;
889
1392
  }
890
1393
 
891
1394
  return size;
892
1395
  },
893
- _getValidationDescriptor: function(file) {
1396
+ _getValidationDescriptor: function(fileOrBlobData) {
894
1397
  var name, size, fileDescriptor;
895
1398
 
896
1399
  fileDescriptor = {};
897
- name = this._parseFileName(file);
898
- size = this._parseFileSize(file);
1400
+ name = this._parseFileOrBlobDataName(fileOrBlobData);
1401
+ size = this._parseFileOrBlobDataSize(fileOrBlobData);
899
1402
 
900
1403
  fileDescriptor.name = name;
901
1404
  if (size) {
@@ -905,16 +1408,433 @@ qq.FineUploaderBasic.prototype = {
905
1408
  return fileDescriptor;
906
1409
  },
907
1410
  _getValidationDescriptors: function(files) {
908
- var index, fileDescriptors;
1411
+ var self = this,
1412
+ fileDescriptors = [];
1413
+
1414
+ qq.each(files, function(idx, file) {
1415
+ fileDescriptors.push(self._getValidationDescriptor(file));
1416
+ });
1417
+
1418
+ return fileDescriptors;
1419
+ },
1420
+ _createParamsStore: function(type) {
1421
+ var paramsStore = {},
1422
+ self = this;
1423
+
1424
+ return {
1425
+ setParams: function(params, id) {
1426
+ var paramsCopy = {};
1427
+ qq.extend(paramsCopy, params);
1428
+ paramsStore[id] = paramsCopy;
1429
+ },
1430
+
1431
+ getParams: function(id) {
1432
+ /*jshint eqeqeq: true, eqnull: true*/
1433
+ var paramsCopy = {};
1434
+
1435
+ if (id != null && paramsStore[id]) {
1436
+ qq.extend(paramsCopy, paramsStore[id]);
1437
+ }
1438
+ else {
1439
+ qq.extend(paramsCopy, self._options[type].params);
1440
+ }
1441
+
1442
+ return paramsCopy;
1443
+ },
1444
+
1445
+ remove: function(fileId) {
1446
+ return delete paramsStore[fileId];
1447
+ },
1448
+
1449
+ reset: function() {
1450
+ paramsStore = {};
1451
+ }
1452
+ };
1453
+ },
1454
+ _createEndpointStore: function(type) {
1455
+ var endpointStore = {},
1456
+ self = this;
1457
+
1458
+ return {
1459
+ setEndpoint: function(endpoint, id) {
1460
+ endpointStore[id] = endpoint;
1461
+ },
1462
+
1463
+ getEndpoint: function(id) {
1464
+ /*jshint eqeqeq: true, eqnull: true*/
1465
+ if (id != null && endpointStore[id]) {
1466
+ return endpointStore[id];
1467
+ }
1468
+
1469
+ return self._options[type].endpoint;
1470
+ },
1471
+
1472
+ remove: function(fileId) {
1473
+ return delete endpointStore[fileId];
1474
+ },
909
1475
 
910
- fileDescriptors = [];
1476
+ reset: function() {
1477
+ endpointStore = {};
1478
+ }
1479
+ };
1480
+ }
1481
+ };
1482
+ /*globals qq, document*/
1483
+ qq.DragAndDrop = function(o) {
1484
+ "use strict";
1485
+
1486
+ var options, dz, dirPending,
1487
+ droppedFiles = [],
1488
+ droppedEntriesCount = 0,
1489
+ droppedEntriesParsedCount = 0,
1490
+ disposeSupport = new qq.DisposeSupport();
911
1491
 
912
- for (index = 0; index < files.length; index++) {
913
- fileDescriptors.push(files[index]);
1492
+ options = {
1493
+ dropArea: null,
1494
+ extraDropzones: [],
1495
+ hideDropzones: true,
1496
+ multiple: true,
1497
+ classes: {
1498
+ dropActive: null
1499
+ },
1500
+ callbacks: {
1501
+ dropProcessing: function(isProcessing, files) {},
1502
+ error: function(code, filename) {},
1503
+ log: function(message, level) {}
914
1504
  }
1505
+ };
915
1506
 
916
- return fileDescriptors;
1507
+ qq.extend(options, o);
1508
+
1509
+ function maybeUploadDroppedFiles() {
1510
+ if (droppedEntriesCount === droppedEntriesParsedCount && !dirPending) {
1511
+ options.callbacks.log('Grabbed ' + droppedFiles.length + " files after tree traversal.");
1512
+ dz.dropDisabled(false);
1513
+ options.callbacks.dropProcessing(false, droppedFiles);
1514
+ }
1515
+ }
1516
+ function addDroppedFile(file) {
1517
+ droppedFiles.push(file);
1518
+ droppedEntriesParsedCount+=1;
1519
+ maybeUploadDroppedFiles();
1520
+ }
1521
+
1522
+ function traverseFileTree(entry) {
1523
+ var dirReader, i;
1524
+
1525
+ droppedEntriesCount+=1;
1526
+
1527
+ if (entry.isFile) {
1528
+ entry.file(function(file) {
1529
+ addDroppedFile(file);
1530
+ });
1531
+ }
1532
+ else if (entry.isDirectory) {
1533
+ dirPending = true;
1534
+ dirReader = entry.createReader();
1535
+ dirReader.readEntries(function(entries) {
1536
+ droppedEntriesParsedCount+=1;
1537
+ for (i = 0; i < entries.length; i+=1) {
1538
+ traverseFileTree(entries[i]);
1539
+ }
1540
+
1541
+ dirPending = false;
1542
+
1543
+ if (!entries.length) {
1544
+ maybeUploadDroppedFiles();
1545
+ }
1546
+ });
1547
+ }
1548
+ }
1549
+
1550
+ function handleDataTransfer(dataTransfer) {
1551
+ var i, items, entry;
1552
+
1553
+ options.callbacks.dropProcessing(true);
1554
+ dz.dropDisabled(true);
1555
+
1556
+ if (dataTransfer.files.length > 1 && !options.multiple) {
1557
+ options.callbacks.dropProcessing(false);
1558
+ options.callbacks.error('tooManyFilesError', "");
1559
+ dz.dropDisabled(false);
1560
+ }
1561
+ else {
1562
+ droppedFiles = [];
1563
+ droppedEntriesCount = 0;
1564
+ droppedEntriesParsedCount = 0;
1565
+
1566
+ if (qq.isFolderDropSupported(dataTransfer)) {
1567
+ items = dataTransfer.items;
1568
+
1569
+ for (i = 0; i < items.length; i+=1) {
1570
+ entry = items[i].webkitGetAsEntry();
1571
+ if (entry) {
1572
+ //due to a bug in Chrome's File System API impl - #149735
1573
+ if (entry.isFile) {
1574
+ droppedFiles.push(items[i].getAsFile());
1575
+ if (i === items.length-1) {
1576
+ maybeUploadDroppedFiles();
1577
+ }
1578
+ }
1579
+
1580
+ else {
1581
+ traverseFileTree(entry);
1582
+ }
1583
+ }
1584
+ }
1585
+ }
1586
+ else {
1587
+ options.callbacks.dropProcessing(false, dataTransfer.files);
1588
+ dz.dropDisabled(false);
1589
+ }
1590
+ }
1591
+ }
1592
+
1593
+ function setupDropzone(dropArea){
1594
+ dz = new qq.UploadDropZone({
1595
+ element: dropArea,
1596
+ onEnter: function(e){
1597
+ qq(dropArea).addClass(options.classes.dropActive);
1598
+ e.stopPropagation();
1599
+ },
1600
+ onLeaveNotDescendants: function(e){
1601
+ qq(dropArea).removeClass(options.classes.dropActive);
1602
+ },
1603
+ onDrop: function(e){
1604
+ if (options.hideDropzones) {
1605
+ qq(dropArea).hide();
1606
+ }
1607
+ qq(dropArea).removeClass(options.classes.dropActive);
1608
+
1609
+ handleDataTransfer(e.dataTransfer);
1610
+ }
1611
+ });
1612
+
1613
+ disposeSupport.addDisposer(function() {
1614
+ dz.dispose();
1615
+ });
1616
+
1617
+ if (options.hideDropzones) {
1618
+ qq(dropArea).hide();
1619
+ }
1620
+ }
1621
+
1622
+ function isFileDrag(dragEvent) {
1623
+ var fileDrag;
1624
+
1625
+ qq.each(dragEvent.dataTransfer.types, function(key, val) {
1626
+ if (val === 'Files') {
1627
+ fileDrag = true;
1628
+ return false;
1629
+ }
1630
+ });
1631
+
1632
+ return fileDrag;
1633
+ }
1634
+
1635
+ function setupDragDrop(){
1636
+ if (options.dropArea) {
1637
+ options.extraDropzones.push(options.dropArea);
1638
+ }
1639
+
1640
+ var i, dropzones = options.extraDropzones;
1641
+
1642
+ for (i=0; i < dropzones.length; i+=1){
1643
+ setupDropzone(dropzones[i]);
1644
+ }
1645
+
1646
+ // IE <= 9 does not support the File API used for drag+drop uploads
1647
+ if (options.dropArea && (!qq.ie() || qq.ie10())) {
1648
+ disposeSupport.attach(document, 'dragenter', function(e) {
1649
+ if (!dz.dropDisabled() && isFileDrag(e)) {
1650
+ if (qq(options.dropArea).hasClass(options.classes.dropDisabled)) {
1651
+ return;
1652
+ }
1653
+
1654
+ options.dropArea.style.display = 'block';
1655
+ for (i=0; i < dropzones.length; i+=1) {
1656
+ dropzones[i].style.display = 'block';
1657
+ }
1658
+ }
1659
+ });
1660
+ }
1661
+ disposeSupport.attach(document, 'dragleave', function(e){
1662
+ if (options.hideDropzones && qq.FineUploader.prototype._leaving_document_out(e)) {
1663
+ for (i=0; i < dropzones.length; i+=1) {
1664
+ qq(dropzones[i]).hide();
1665
+ }
1666
+ }
1667
+ });
1668
+ disposeSupport.attach(document, 'drop', function(e){
1669
+ if (options.hideDropzones) {
1670
+ for (i=0; i < dropzones.length; i+=1) {
1671
+ qq(dropzones[i]).hide();
1672
+ }
1673
+ }
1674
+ e.preventDefault();
1675
+ });
1676
+ }
1677
+
1678
+ return {
1679
+ setup: function() {
1680
+ setupDragDrop();
1681
+ },
1682
+
1683
+ setupExtraDropzone: function(element) {
1684
+ options.extraDropzones.push(element);
1685
+ setupDropzone(element);
1686
+ },
1687
+
1688
+ removeExtraDropzone: function(element) {
1689
+ var i, dzs = options.extraDropzones;
1690
+ for(i in dzs) {
1691
+ if (dzs[i] === element) {
1692
+ return dzs.splice(i, 1);
1693
+ }
1694
+ }
1695
+ },
1696
+
1697
+ dispose: function() {
1698
+ disposeSupport.dispose();
1699
+ dz.dispose();
1700
+ }
1701
+ };
1702
+ };
1703
+
1704
+
1705
+ qq.UploadDropZone = function(o){
1706
+ "use strict";
1707
+
1708
+ var options, element, preventDrop, dropOutsideDisabled, disposeSupport = new qq.DisposeSupport();
1709
+
1710
+ options = {
1711
+ element: null,
1712
+ onEnter: function(e){},
1713
+ onLeave: function(e){},
1714
+ // is not fired when leaving element by hovering descendants
1715
+ onLeaveNotDescendants: function(e){},
1716
+ onDrop: function(e){}
1717
+ };
1718
+
1719
+ qq.extend(options, o);
1720
+ element = options.element;
1721
+
1722
+ function dragover_should_be_canceled(){
1723
+ return qq.safari() || (qq.firefox() && qq.windows());
1724
+ }
1725
+
1726
+ function disableDropOutside(e){
1727
+ // run only once for all instances
1728
+ if (!dropOutsideDisabled ){
1729
+
1730
+ // for these cases we need to catch onDrop to reset dropArea
1731
+ if (dragover_should_be_canceled){
1732
+ disposeSupport.attach(document, 'dragover', function(e){
1733
+ e.preventDefault();
1734
+ });
1735
+ } else {
1736
+ disposeSupport.attach(document, 'dragover', function(e){
1737
+ if (e.dataTransfer){
1738
+ e.dataTransfer.dropEffect = 'none';
1739
+ e.preventDefault();
1740
+ }
1741
+ });
1742
+ }
1743
+
1744
+ dropOutsideDisabled = true;
1745
+ }
1746
+ }
1747
+
1748
+ function isValidFileDrag(e){
1749
+ // e.dataTransfer currently causing IE errors
1750
+ // IE9 does NOT support file API, so drag-and-drop is not possible
1751
+ if (qq.ie() && !qq.ie10()) {
1752
+ return false;
1753
+ }
1754
+
1755
+ var effectTest, dt = e.dataTransfer,
1756
+ // do not check dt.types.contains in webkit, because it crashes safari 4
1757
+ isSafari = qq.safari();
1758
+
1759
+ // dt.effectAllowed is none in Safari 5
1760
+ // dt.types.contains check is for firefox
1761
+ effectTest = qq.ie10() ? true : dt.effectAllowed !== 'none';
1762
+ return dt && effectTest && (dt.files || (!isSafari && dt.types.contains && dt.types.contains('Files')));
1763
+ }
1764
+
1765
+ function isOrSetDropDisabled(isDisabled) {
1766
+ if (isDisabled !== undefined) {
1767
+ preventDrop = isDisabled;
1768
+ }
1769
+ return preventDrop;
1770
+ }
1771
+
1772
+ function attachEvents(){
1773
+ disposeSupport.attach(element, 'dragover', function(e){
1774
+ if (!isValidFileDrag(e)) {
1775
+ return;
1776
+ }
1777
+
1778
+ var effect = qq.ie() ? null : e.dataTransfer.effectAllowed;
1779
+ if (effect === 'move' || effect === 'linkMove'){
1780
+ e.dataTransfer.dropEffect = 'move'; // for FF (only move allowed)
1781
+ } else {
1782
+ e.dataTransfer.dropEffect = 'copy'; // for Chrome
1783
+ }
1784
+
1785
+ e.stopPropagation();
1786
+ e.preventDefault();
1787
+ });
1788
+
1789
+ disposeSupport.attach(element, 'dragenter', function(e){
1790
+ if (!isOrSetDropDisabled()) {
1791
+ if (!isValidFileDrag(e)) {
1792
+ return;
1793
+ }
1794
+ options.onEnter(e);
1795
+ }
1796
+ });
1797
+
1798
+ disposeSupport.attach(element, 'dragleave', function(e){
1799
+ if (!isValidFileDrag(e)) {
1800
+ return;
1801
+ }
1802
+
1803
+ options.onLeave(e);
1804
+
1805
+ var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);
1806
+ // do not fire when moving a mouse over a descendant
1807
+ if (qq(this).contains(relatedTarget)) {
1808
+ return;
1809
+ }
1810
+
1811
+ options.onLeaveNotDescendants(e);
1812
+ });
1813
+
1814
+ disposeSupport.attach(element, 'drop', function(e){
1815
+ if (!isOrSetDropDisabled()) {
1816
+ if (!isValidFileDrag(e)) {
1817
+ return;
1818
+ }
1819
+
1820
+ e.preventDefault();
1821
+ options.onDrop(e);
1822
+ }
1823
+ });
917
1824
  }
1825
+
1826
+ disableDropOutside();
1827
+ attachEvents();
1828
+
1829
+ return {
1830
+ dropDisabled: function(isDisabled) {
1831
+ return isOrSetDropDisabled(isDisabled);
1832
+ },
1833
+
1834
+ dispose: function() {
1835
+ disposeSupport.dispose();
1836
+ }
1837
+ };
918
1838
  };
919
1839
  /**
920
1840
  * Class that creates upload widget with drag-and-drop and file list
@@ -937,14 +1857,17 @@ qq.FineUploader = function(o){
937
1857
  uploadButton: 'Upload a file',
938
1858
  cancelButton: 'Cancel',
939
1859
  retryButton: 'Retry',
1860
+ deleteButton: 'Delete',
940
1861
  failUpload: 'Upload failed',
941
1862
  dragZone: 'Drop files here to upload',
1863
+ dropProcessing: 'Processing dropped files...',
942
1864
  formatProgress: "{percent}% of {total_size}",
943
1865
  waitingForResponse: "Processing..."
944
1866
  },
945
1867
  template: '<div class="qq-uploader">' +
946
1868
  ((!this._options.dragAndDrop || !this._options.dragAndDrop.disableDefaultDropzone) ? '<div class="qq-upload-drop-area"><span>{dragZoneText}</span></div>' : '') +
947
1869
  (!this._options.button ? '<div class="qq-upload-button"><div>{uploadButtonText}</div></div>' : '') +
1870
+ '<span class="qq-drop-processing"><span>{dropProcessingText}</span><span class="qq-drop-processing-spinner"></span></span>' +
948
1871
  (!this._options.listElement ? '<ul class="qq-upload-list"></ul>' : '') +
949
1872
  '</div>',
950
1873
 
@@ -957,10 +1880,10 @@ qq.FineUploader = function(o){
957
1880
  '<span class="qq-upload-size"></span>' +
958
1881
  '<a class="qq-upload-cancel" href="#">{cancelButtonText}</a>' +
959
1882
  '<a class="qq-upload-retry" href="#">{retryButtonText}</a>' +
1883
+ '<a class="qq-upload-delete" href="#">{deleteButtonText}</a>' +
960
1884
  '<span class="qq-upload-status-text">{statusText}</span>' +
961
1885
  '</li>',
962
1886
  classes: {
963
- // used to get elements from templates
964
1887
  button: 'qq-upload-button',
965
1888
  drop: 'qq-upload-drop-area',
966
1889
  dropActive: 'qq-upload-drop-area-active',
@@ -974,16 +1897,18 @@ qq.FineUploader = function(o){
974
1897
  retryable: 'qq-upload-retryable',
975
1898
  size: 'qq-upload-size',
976
1899
  cancel: 'qq-upload-cancel',
1900
+ deleteButton: 'qq-upload-delete',
977
1901
  retry: 'qq-upload-retry',
978
1902
  statusText: 'qq-upload-status-text',
979
1903
 
980
- // added to list item <li> when upload completes
981
- // used in css to hide progress spinner
982
1904
  success: 'qq-upload-success',
983
1905
  fail: 'qq-upload-fail',
984
1906
 
985
1907
  successIcon: null,
986
- failIcon: null
1908
+ failIcon: null,
1909
+
1910
+ dropProcessing: 'qq-drop-processing',
1911
+ dropProcessingSpinner: 'qq-drop-processing-spinner'
987
1912
  },
988
1913
  failedUploadTextDisplay: {
989
1914
  mode: 'default', //default, custom, or none
@@ -999,8 +1924,31 @@ qq.FineUploader = function(o){
999
1924
  autoRetryNote: "Retrying {retryNum}/{maxAuto}...",
1000
1925
  showButton: false
1001
1926
  },
1927
+ deleteFile: {
1928
+ forceConfirm: false,
1929
+ confirmMessage: "Are you sure you want to delete {filename}?",
1930
+ deletingStatusText: "Deleting...",
1931
+ deletingFailedText: "Delete failed"
1932
+
1933
+ },
1934
+ display: {
1935
+ fileSizeOnSubmit: false
1936
+ },
1002
1937
  showMessage: function(message){
1003
- alert(message);
1938
+ setTimeout(function() {
1939
+ alert(message);
1940
+ }, 0);
1941
+ },
1942
+ showConfirm: function(message, okCallback, cancelCallback) {
1943
+ setTimeout(function() {
1944
+ var result = confirm(message);
1945
+ if (result) {
1946
+ okCallback();
1947
+ }
1948
+ else if (cancelCallback) {
1949
+ cancelCallback();
1950
+ }
1951
+ }, 0);
1004
1952
  }
1005
1953
  }, true);
1006
1954
 
@@ -1012,8 +1960,10 @@ qq.FineUploader = function(o){
1012
1960
  // same for the Cancel button and Fail message text
1013
1961
  this._options.template = this._options.template.replace(/\{dragZoneText\}/g, this._options.text.dragZone);
1014
1962
  this._options.template = this._options.template.replace(/\{uploadButtonText\}/g, this._options.text.uploadButton);
1963
+ this._options.template = this._options.template.replace(/\{dropProcessingText\}/g, this._options.text.dropProcessing);
1015
1964
  this._options.fileTemplate = this._options.fileTemplate.replace(/\{cancelButtonText\}/g, this._options.text.cancelButton);
1016
1965
  this._options.fileTemplate = this._options.fileTemplate.replace(/\{retryButtonText\}/g, this._options.text.retryButton);
1966
+ this._options.fileTemplate = this._options.fileTemplate.replace(/\{deleteButtonText\}/g, this._options.text.deleteButton);
1017
1967
  this._options.fileTemplate = this._options.fileTemplate.replace(/\{statusText\}/g, "");
1018
1968
 
1019
1969
  this._element = this._options.element;
@@ -1027,7 +1977,8 @@ qq.FineUploader = function(o){
1027
1977
  }
1028
1978
 
1029
1979
  this._bindCancelAndRetryEvents();
1030
- this._setupDragDrop();
1980
+
1981
+ this._dnd = this._setupDragAndDrop();
1031
1982
  };
1032
1983
 
1033
1984
  // inherit from Basic Uploader
@@ -1039,11 +1990,10 @@ qq.extend(qq.FineUploader.prototype, {
1039
1990
  this._listElement.innerHTML = "";
1040
1991
  },
1041
1992
  addExtraDropzone: function(element){
1042
- this._setupExtraDropzone(element);
1993
+ this._dnd.setupExtraDropzone(element);
1043
1994
  },
1044
1995
  removeExtraDropzone: function(element){
1045
- var dzs = this._options.dragAndDrop.extraDropzones;
1046
- for(var i in dzs) if (dzs[i] === element) return this._options.dragAndDrop.extraDropzones.splice(i,1);
1996
+ return this._dnd.removeExtraDropzone(element);
1047
1997
  },
1048
1998
  getItemByFileId: function(id){
1049
1999
  var item = this._listElement.firstChild;
@@ -1063,119 +2013,93 @@ qq.extend(qq.FineUploader.prototype, {
1063
2013
  this._button = this._createUploadButton(this._find(this._element, 'button'));
1064
2014
  }
1065
2015
  this._bindCancelAndRetryEvents();
1066
- this._setupDragDrop();
2016
+ this._dnd.dispose();
2017
+ this._dnd = this._setupDragAndDrop();
1067
2018
  },
1068
- _leaving_document_out: function(e){
1069
- return ((qq.chrome() || (qq.safari() && qq.windows())) && e.clientX == 0 && e.clientY == 0) // null coords for Chrome and Safari Windows
1070
- || (qq.firefox() && !e.relatedTarget); // null e.relatedTarget for Firefox
2019
+ _removeFileItem: function(fileId) {
2020
+ var item = this.getItemByFileId(fileId);
2021
+ qq(item).remove();
1071
2022
  },
1072
- _storeFileForLater: function(id) {
1073
- qq.FineUploaderBasic.prototype._storeFileForLater.apply(this, arguments);
1074
- var item = this.getItemByFileId(id);
1075
- qq(this._find(item, 'spinner')).hide();
1076
- },
1077
- /**
1078
- * Gets one of the elements listed in this._options.classes
1079
- **/
1080
- _find: function(parent, type){
1081
- var element = qq(parent).getByClass(this._options.classes[type])[0];
1082
- if (!element){
1083
- throw new Error('element not found ' + type);
1084
- }
2023
+ _setupDragAndDrop: function() {
2024
+ var self = this,
2025
+ dropProcessingEl = this._find(this._element, 'dropProcessing'),
2026
+ dnd, preventSelectFiles, defaultDropAreaEl;
1085
2027
 
1086
- return element;
1087
- },
1088
- _setupExtraDropzone: function(element){
1089
- this._options.dragAndDrop.extraDropzones.push(element);
1090
- this._setupDropzone(element);
1091
- },
1092
- _setupDropzone: function(dropArea){
1093
- var self = this;
2028
+ preventSelectFiles = function(event) {
2029
+ event.preventDefault();
2030
+ };
1094
2031
 
1095
- var dz = new qq.UploadDropZone({
1096
- element: dropArea,
1097
- onEnter: function(e){
1098
- qq(dropArea).addClass(self._classes.dropActive);
1099
- e.stopPropagation();
1100
- },
1101
- onLeave: function(e){
1102
- //e.stopPropagation();
1103
- },
1104
- onLeaveNotDescendants: function(e){
1105
- qq(dropArea).removeClass(self._classes.dropActive);
2032
+ if (!this._options.dragAndDrop.disableDefaultDropzone) {
2033
+ defaultDropAreaEl = this._find(this._options.element, 'drop');
2034
+ }
2035
+
2036
+ dnd = new qq.DragAndDrop({
2037
+ dropArea: defaultDropAreaEl,
2038
+ extraDropzones: this._options.dragAndDrop.extraDropzones,
2039
+ hideDropzones: this._options.dragAndDrop.hideDropzones,
2040
+ multiple: this._options.multiple,
2041
+ classes: {
2042
+ dropActive: this._options.classes.dropActive
1106
2043
  },
1107
- onDrop: function(e){
1108
- if (self._options.dragAndDrop.hideDropzones) {
1109
- qq(dropArea).hide();
1110
- }
2044
+ callbacks: {
2045
+ dropProcessing: function(isProcessing, files) {
2046
+ var input = self._button.getInput();
1111
2047
 
1112
- qq(dropArea).removeClass(self._classes.dropActive);
1113
- if (e.dataTransfer.files.length > 1 && !self._options.multiple) {
1114
- self._error('tooManyFilesError', "");
1115
- }
1116
- else {
1117
- self._uploadFileList(e.dataTransfer.files);
2048
+ if (isProcessing) {
2049
+ qq(dropProcessingEl).css({display: 'block'});
2050
+ qq(input).attach('click', preventSelectFiles);
2051
+ }
2052
+ else {
2053
+ qq(dropProcessingEl).hide();
2054
+ qq(input).detach('click', preventSelectFiles);
2055
+ }
2056
+
2057
+ if (files) {
2058
+ self.addFiles(files);
2059
+ }
2060
+ },
2061
+ error: function(code, filename) {
2062
+ self._error(code, filename);
2063
+ },
2064
+ log: function(message, level) {
2065
+ self.log(message, level);
1118
2066
  }
1119
2067
  }
1120
2068
  });
1121
2069
 
1122
- this.addDisposer(function() { dz.dispose(); });
2070
+ dnd.setup();
1123
2071
 
1124
- if (this._options.dragAndDrop.hideDropzones) {
1125
- qq(dropArea).hide();
1126
- }
2072
+ return dnd;
1127
2073
  },
1128
- _setupDragDrop: function(){
1129
- var self, dropArea;
1130
-
1131
- self = this;
1132
-
1133
- if (!this._options.dragAndDrop.disableDefaultDropzone) {
1134
- dropArea = this._find(this._element, 'drop');
1135
- this._options.dragAndDrop.extraDropzones.push(dropArea);
1136
- }
1137
-
1138
- var dropzones = this._options.dragAndDrop.extraDropzones;
1139
- var i;
1140
- for (i=0; i < dropzones.length; i++){
1141
- this._setupDropzone(dropzones[i]);
2074
+ _leaving_document_out: function(e){
2075
+ return ((qq.chrome() || (qq.safari() && qq.windows())) && e.clientX == 0 && e.clientY == 0) // null coords for Chrome and Safari Windows
2076
+ || (qq.firefox() && !e.relatedTarget); // null e.relatedTarget for Firefox
2077
+ },
2078
+ _storeForLater: function(id) {
2079
+ qq.FineUploaderBasic.prototype._storeForLater.apply(this, arguments);
2080
+ var item = this.getItemByFileId(id);
2081
+ qq(this._find(item, 'spinner')).hide();
2082
+ },
2083
+ /**
2084
+ * Gets one of the elements listed in this._options.classes
2085
+ **/
2086
+ _find: function(parent, type){
2087
+ var element = qq(parent).getByClass(this._options.classes[type])[0];
2088
+ if (!element){
2089
+ throw new Error('element not found ' + type);
1142
2090
  }
1143
2091
 
1144
- // IE <= 9 does not support the File API used for drag+drop uploads
1145
- if (!this._options.dragAndDrop.disableDefaultDropzone && (!qq.ie() || qq.ie10())) {
1146
- this._attach(document, 'dragenter', function(e){
1147
- if (qq(dropArea).hasClass(self._classes.dropDisabled)) return;
1148
-
1149
- dropArea.style.display = 'block';
1150
- for (i=0; i < dropzones.length; i++){ dropzones[i].style.display = 'block'; }
1151
-
1152
- });
1153
- }
1154
- this._attach(document, 'dragleave', function(e){
1155
- if (self._options.dragAndDrop.hideDropzones && qq.FineUploader.prototype._leaving_document_out(e)) {
1156
- for (i=0; i < dropzones.length; i++) {
1157
- qq(dropzones[i]).hide();
1158
- }
1159
- }
1160
- });
1161
- qq(document).attach('drop', function(e){
1162
- if (self._options.dragAndDrop.hideDropzones) {
1163
- for (i=0; i < dropzones.length; i++) {
1164
- qq(dropzones[i]).hide();
1165
- }
1166
- }
1167
- e.preventDefault();
1168
- });
2092
+ return element;
1169
2093
  },
1170
- _onSubmit: function(id, fileName){
2094
+ _onSubmit: function(id, name){
1171
2095
  qq.FineUploaderBasic.prototype._onSubmit.apply(this, arguments);
1172
- this._addToList(id, fileName);
2096
+ this._addToList(id, name);
1173
2097
  },
1174
2098
  // Update the progress bar & percentage as the file is uploaded
1175
- _onProgress: function(id, fileName, loaded, total){
2099
+ _onProgress: function(id, name, loaded, total){
1176
2100
  qq.FineUploaderBasic.prototype._onProgress.apply(this, arguments);
1177
2101
 
1178
- var item, progressBar, text, percent, cancelLink, size;
2102
+ var item, progressBar, percent, cancelLink;
1179
2103
 
1180
2104
  item = this.getItemByFileId(id);
1181
2105
  progressBar = this._find(item, 'progressBar');
@@ -1188,24 +2112,20 @@ qq.extend(qq.FineUploader.prototype, {
1188
2112
  qq(progressBar).hide();
1189
2113
  qq(this._find(item, 'statusText')).setText(this._options.text.waitingForResponse);
1190
2114
 
1191
- // If last byte was sent, just display final size
1192
- text = this._formatSize(total);
2115
+ // If last byte was sent, display total file size
2116
+ this._displayFileSize(id);
1193
2117
  }
1194
2118
  else {
1195
- // If still uploading, display percentage
1196
- text = this._formatProgress(loaded, total);
2119
+ // If still uploading, display percentage - total size is actually the total request(s) size
2120
+ this._displayFileSize(id, loaded, total);
1197
2121
 
1198
2122
  qq(progressBar).css({display: 'block'});
1199
2123
  }
1200
2124
 
1201
2125
  // Update progress bar element
1202
2126
  qq(progressBar).css({width: percent + '%'});
1203
-
1204
- size = this._find(item, 'size');
1205
- qq(size).css({display: 'inline'});
1206
- qq(size).setText(text);
1207
2127
  },
1208
- _onComplete: function(id, fileName, result, xhr){
2128
+ _onComplete: function(id, name, result, xhr){
1209
2129
  qq.FineUploaderBasic.prototype._onComplete.apply(this, arguments);
1210
2130
 
1211
2131
  var item = this.getItemByFileId(id);
@@ -1215,12 +2135,16 @@ qq.extend(qq.FineUploader.prototype, {
1215
2135
  qq(item).removeClass(this._classes.retrying);
1216
2136
  qq(this._find(item, 'progressBar')).hide();
1217
2137
 
1218
- if (!this._options.disableCancelForFormUploads || qq.UploadHandlerXhr.isSupported()) {
2138
+ if (!this._options.disableCancelForFormUploads || qq.isXhrUploadSupported()) {
1219
2139
  qq(this._find(item, 'cancel')).hide();
1220
2140
  }
1221
2141
  qq(this._find(item, 'spinner')).hide();
1222
2142
 
1223
- if (result.success){
2143
+ if (result.success) {
2144
+ if (this._isDeletePossible()) {
2145
+ this._showDeleteLink(id);
2146
+ }
2147
+
1224
2148
  qq(item).addClass(this._classes.success);
1225
2149
  if (this._classes.successIcon) {
1226
2150
  this._find(item, 'finished').style.display = "inline-block";
@@ -1238,14 +2162,17 @@ qq.extend(qq.FineUploader.prototype, {
1238
2162
  this._controlFailureTextDisplay(item, result);
1239
2163
  }
1240
2164
  },
1241
- _onUpload: function(id, fileName, xhr){
2165
+ _onUpload: function(id, name){
1242
2166
  qq.FineUploaderBasic.prototype._onUpload.apply(this, arguments);
1243
2167
 
1244
- var item = this.getItemByFileId(id);
1245
- this._showSpinner(item);
2168
+ this._showSpinner(id);
2169
+ },
2170
+ _onCancel: function(id, name) {
2171
+ qq.FineUploaderBasic.prototype._onCancel.apply(this, arguments);
2172
+ this._removeFileItem(id);
1246
2173
  },
1247
2174
  _onBeforeAutoRetry: function(id) {
1248
- var item, progressBar, cancelLink, failTextEl, retryNumForDisplay, maxAuto, retryNote;
2175
+ var item, progressBar, failTextEl, retryNumForDisplay, maxAuto, retryNote;
1249
2176
 
1250
2177
  qq.FineUploaderBasic.prototype._onBeforeAutoRetry.apply(this, arguments);
1251
2178
 
@@ -1276,15 +2203,69 @@ qq.extend(qq.FineUploader.prototype, {
1276
2203
  var item = this.getItemByFileId(id);
1277
2204
  this._find(item, 'progressBar').style.width = 0;
1278
2205
  qq(item).removeClass(this._classes.fail);
1279
- this._showSpinner(item);
2206
+ qq(this._find(item, 'statusText')).clearText();
2207
+ this._showSpinner(id);
1280
2208
  this._showCancelLink(item);
1281
2209
  return true;
1282
2210
  }
1283
2211
  return false;
1284
2212
  },
1285
- _addToList: function(id, fileName){
2213
+ _onSubmitDelete: function(id) {
2214
+ if (this._isDeletePossible()) {
2215
+ if (this._options.callbacks.onSubmitDelete(id) !== false) {
2216
+ if (this._options.deleteFile.forceConfirm) {
2217
+ this._showDeleteConfirm(id);
2218
+ }
2219
+ else {
2220
+ this._sendDeleteRequest(id);
2221
+ }
2222
+ }
2223
+ }
2224
+ else {
2225
+ this.log("Delete request ignored for file ID " + id + ", delete feature is disabled.", "warn");
2226
+ return false;
2227
+ }
2228
+ },
2229
+ _onDeleteComplete: function(id, xhr, isError) {
2230
+ qq.FineUploaderBasic.prototype._onDeleteComplete.apply(this, arguments);
2231
+
2232
+ var item = this.getItemByFileId(id),
2233
+ spinnerEl = this._find(item, 'spinner'),
2234
+ statusTextEl = this._find(item, 'statusText');
2235
+
2236
+ qq(spinnerEl).hide();
2237
+
2238
+ if (isError) {
2239
+ qq(statusTextEl).setText(this._options.deleteFile.deletingFailedText);
2240
+ this._showDeleteLink(id);
2241
+ }
2242
+ else {
2243
+ this._removeFileItem(id);
2244
+ }
2245
+ },
2246
+ _sendDeleteRequest: function(id) {
2247
+ var item = this.getItemByFileId(id),
2248
+ deleteLink = this._find(item, 'deleteButton'),
2249
+ statusTextEl = this._find(item, 'statusText');
2250
+
2251
+ qq(deleteLink).hide();
2252
+ this._showSpinner(id);
2253
+ qq(statusTextEl).setText(this._options.deleteFile.deletingStatusText);
2254
+ this._deleteHandler.sendDelete(id, this.getUuid(id));
2255
+ },
2256
+ _showDeleteConfirm: function(id) {
2257
+ var fileName = this._handler.getName(id),
2258
+ confirmMessage = this._options.deleteFile.confirmMessage.replace(/\{filename\}/g, fileName),
2259
+ uuid = this.getUuid(id),
2260
+ self = this;
2261
+
2262
+ this._options.showConfirm(confirmMessage, function() {
2263
+ self._sendDeleteRequest(id);
2264
+ });
2265
+ },
2266
+ _addToList: function(id, name){
1286
2267
  var item = qq.toElement(this._options.fileTemplate);
1287
- if (this._options.disableCancelForFormUploads && !qq.UploadHandlerXhr.isSupported()) {
2268
+ if (this._options.disableCancelForFormUploads && !qq.isXhrUploadSupported()) {
1288
2269
  var cancelLink = this._find(item, 'cancel');
1289
2270
  qq(cancelLink).remove();
1290
2271
  }
@@ -1292,15 +2273,36 @@ qq.extend(qq.FineUploader.prototype, {
1292
2273
  item.qqFileId = id;
1293
2274
 
1294
2275
  var fileElement = this._find(item, 'file');
1295
- qq(fileElement).setText(this._formatFileName(fileName));
2276
+ qq(fileElement).setText(this._options.formatFileName(name));
1296
2277
  qq(this._find(item, 'size')).hide();
1297
- if (!this._options.multiple) this._clearList();
2278
+ if (!this._options.multiple) {
2279
+ this._handler.cancelAll();
2280
+ this._clearList();
2281
+ }
2282
+
1298
2283
  this._listElement.appendChild(item);
2284
+
2285
+ if (this._options.display.fileSizeOnSubmit && qq.isXhrUploadSupported()) {
2286
+ this._displayFileSize(id);
2287
+ }
1299
2288
  },
1300
2289
  _clearList: function(){
1301
2290
  this._listElement.innerHTML = '';
1302
2291
  this.clearStoredFiles();
1303
2292
  },
2293
+ _displayFileSize: function(id, loadedSize, totalSize) {
2294
+ var item = this.getItemByFileId(id),
2295
+ size = this.getSize(id),
2296
+ sizeForDisplay = this._formatSize(size),
2297
+ sizeEl = this._find(item, 'size');
2298
+
2299
+ if (loadedSize !== undefined && totalSize !== undefined) {
2300
+ sizeForDisplay = this._formatProgress(loadedSize, totalSize);
2301
+ }
2302
+
2303
+ qq(sizeEl).css({display: 'inline'});
2304
+ qq(sizeEl).setText(sizeForDisplay);
2305
+ },
1304
2306
  /**
1305
2307
  * delegate click event for cancel & retry links
1306
2308
  **/
@@ -1308,21 +2310,23 @@ qq.extend(qq.FineUploader.prototype, {
1308
2310
  var self = this,
1309
2311
  list = this._listElement;
1310
2312
 
1311
- this._attach(list, 'click', function(e){
2313
+ this._disposeSupport.attach(list, 'click', function(e){
1312
2314
  e = e || window.event;
1313
2315
  var target = e.target || e.srcElement;
1314
2316
 
1315
- if (qq(target).hasClass(self._classes.cancel) || qq(target).hasClass(self._classes.retry)){
2317
+ if (qq(target).hasClass(self._classes.cancel) || qq(target).hasClass(self._classes.retry) || qq(target).hasClass(self._classes.deleteButton)){
1316
2318
  qq.preventDefault(e);
1317
2319
 
1318
2320
  var item = target.parentNode;
1319
- while(item.qqFileId == undefined) {
2321
+ while(item.qqFileId === undefined) {
1320
2322
  item = target = target.parentNode;
1321
2323
  }
1322
2324
 
1323
- if (qq(target).hasClass(self._classes.cancel)) {
2325
+ if (qq(target).hasClass(self._classes.deleteButton)) {
2326
+ self.deleteFile(item.qqFileId);
2327
+ }
2328
+ else if (qq(target).hasClass(self._classes.cancel)) {
1324
2329
  self.cancel(item.qqFileId);
1325
- qq(item).remove();
1326
2330
  }
1327
2331
  else {
1328
2332
  qq(item).removeClass(self._classes.retryable);
@@ -1371,405 +2375,620 @@ qq.extend(qq.FineUploader.prototype, {
1371
2375
  this.log("failedUploadTextDisplay.mode value of '" + mode + "' is not valid", 'warn');
1372
2376
  }
1373
2377
  },
1374
- //TODO turn this into a real tooltip, with click trigger (so it is usable on mobile devices). See case #355 for details.
1375
2378
  _showTooltip: function(item, text) {
1376
2379
  item.title = text;
1377
2380
  },
1378
- _showSpinner: function(item) {
1379
- var spinnerEl = this._find(item, 'spinner');
2381
+ _showSpinner: function(id) {
2382
+ var item = this.getItemByFileId(id),
2383
+ spinnerEl = this._find(item, 'spinner');
2384
+
1380
2385
  spinnerEl.style.display = "inline-block";
1381
2386
  },
1382
2387
  _showCancelLink: function(item) {
1383
- if (!this._options.disableCancelForFormUploads || qq.UploadHandlerXhr.isSupported()) {
2388
+ if (!this._options.disableCancelForFormUploads || qq.isXhrUploadSupported()) {
1384
2389
  var cancelLink = this._find(item, 'cancel');
1385
- cancelLink.style.display = 'inline';
2390
+
2391
+ qq(cancelLink).css({display: 'inline'});
1386
2392
  }
1387
2393
  },
1388
- _error: function(code, fileName){
2394
+ _showDeleteLink: function(id) {
2395
+ var item = this.getItemByFileId(id),
2396
+ deleteLink = this._find(item, 'deleteButton');
2397
+
2398
+ qq(deleteLink).css({display: 'inline'});
2399
+ },
2400
+ _error: function(code, name){
1389
2401
  var message = qq.FineUploaderBasic.prototype._error.apply(this, arguments);
1390
2402
  this._options.showMessage(message);
1391
2403
  }
1392
2404
  });
2405
+ /** Generic class for sending non-upload ajax requests and handling the associated responses **/
2406
+ //TODO Use XDomainRequest if expectCors = true. Not necessary now since only DELETE requests are sent and XDR doesn't support pre-flighting.
2407
+ /*globals qq, XMLHttpRequest*/
2408
+ qq.AjaxRequestor = function(o) {
2409
+ "use strict";
1393
2410
 
1394
- qq.UploadDropZone = function(o){
1395
- this._options = {
1396
- element: null,
1397
- onEnter: function(e){},
1398
- onLeave: function(e){},
1399
- // is not fired when leaving element by hovering descendants
1400
- onLeaveNotDescendants: function(e){},
1401
- onDrop: function(e){}
1402
- };
1403
- qq.extend(this._options, o);
1404
- qq.extend(this, qq.DisposeSupport);
2411
+ var log, shouldParamsBeInQueryString,
2412
+ queue = [],
2413
+ requestState = [],
2414
+ options = {
2415
+ method: 'POST',
2416
+ maxConnections: 3,
2417
+ customHeaders: {},
2418
+ endpointStore: {},
2419
+ paramsStore: {},
2420
+ successfulResponseCodes: [200],
2421
+ demoMode: false,
2422
+ cors: {
2423
+ expected: false,
2424
+ sendCredentials: false
2425
+ },
2426
+ log: function(str, level) {},
2427
+ onSend: function(id) {},
2428
+ onComplete: function(id, xhr, isError) {},
2429
+ onCancel: function(id) {}
2430
+ };
1405
2431
 
1406
- this._element = this._options.element;
2432
+ qq.extend(options, o);
2433
+ log = options.log;
2434
+ shouldParamsBeInQueryString = getMethod() === 'GET' || getMethod() === 'DELETE';
1407
2435
 
1408
- this._disableDropOutside();
1409
- this._attachEvents();
1410
- };
1411
2436
 
1412
- qq.UploadDropZone.prototype = {
1413
- _dragover_should_be_canceled: function(){
1414
- return qq.safari() || (qq.firefox() && qq.windows());
1415
- },
1416
- _disableDropOutside: function(e){
1417
- // run only once for all instances
1418
- if (!qq.UploadDropZone.dropOutsideDisabled ){
2437
+ /**
2438
+ * Removes element from queue, sends next request
2439
+ */
2440
+ function dequeue(id) {
2441
+ var i = qq.indexOf(queue, id),
2442
+ max = options.maxConnections,
2443
+ nextId;
1419
2444
 
1420
- // for these cases we need to catch onDrop to reset dropArea
1421
- if (this._dragover_should_be_canceled){
1422
- qq(document).attach('dragover', function(e){
1423
- e.preventDefault();
1424
- });
1425
- } else {
1426
- qq(document).attach('dragover', function(e){
1427
- if (e.dataTransfer){
1428
- e.dataTransfer.dropEffect = 'none';
1429
- e.preventDefault();
1430
- }
1431
- });
1432
- }
2445
+ delete requestState[id];
2446
+ queue.splice(i, 1);
1433
2447
 
1434
- qq.UploadDropZone.dropOutsideDisabled = true;
2448
+ if (queue.length >= max && i < max){
2449
+ nextId = queue[max-1];
2450
+ sendRequest(nextId);
1435
2451
  }
1436
- },
1437
- _attachEvents: function(){
1438
- var self = this;
1439
-
1440
- self._attach(self._element, 'dragover', function(e){
1441
- if (!self._isValidFileDrag(e)) return;
2452
+ }
1442
2453
 
1443
- var effect = qq.ie() ? null : e.dataTransfer.effectAllowed;
1444
- if (effect == 'move' || effect == 'linkMove'){
1445
- e.dataTransfer.dropEffect = 'move'; // for FF (only move allowed)
1446
- } else {
1447
- e.dataTransfer.dropEffect = 'copy'; // for Chrome
1448
- }
2454
+ function onComplete(id) {
2455
+ var xhr = requestState[id].xhr,
2456
+ method = getMethod(),
2457
+ isError = false;
1449
2458
 
1450
- e.stopPropagation();
1451
- e.preventDefault();
1452
- });
2459
+ dequeue(id);
1453
2460
 
1454
- self._attach(self._element, 'dragenter', function(e){
1455
- if (!self._isValidFileDrag(e)) return;
2461
+ if (!isResponseSuccessful(xhr.status)) {
2462
+ isError = true;
2463
+ log(method + " request for " + id + " has failed - response code " + xhr.status, "error");
2464
+ }
1456
2465
 
1457
- self._options.onEnter(e);
1458
- });
2466
+ options.onComplete(id, xhr, isError);
2467
+ }
1459
2468
 
1460
- self._attach(self._element, 'dragleave', function(e){
1461
- if (!self._isValidFileDrag(e)) return;
2469
+ function sendRequest(id) {
2470
+ var xhr = new XMLHttpRequest(),
2471
+ method = getMethod(),
2472
+ params = {},
2473
+ url;
1462
2474
 
1463
- self._options.onLeave(e);
2475
+ options.onSend(id);
1464
2476
 
1465
- var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);
1466
- // do not fire when moving a mouse over a descendant
1467
- if (qq(this).contains(relatedTarget)) return;
2477
+ if (options.paramsStore.getParams) {
2478
+ params = options.paramsStore.getParams(id);
2479
+ }
1468
2480
 
1469
- self._options.onLeaveNotDescendants(e);
1470
- });
2481
+ url = createUrl(id, params);
1471
2482
 
1472
- self._attach(self._element, 'drop', function(e){
1473
- if (!self._isValidFileDrag(e)) return;
2483
+ requestState[id].xhr = xhr;
2484
+ xhr.onreadystatechange = getReadyStateChangeHandler(id);
2485
+ xhr.open(method, url, true);
1474
2486
 
1475
- e.preventDefault();
1476
- self._options.onDrop(e);
1477
- });
1478
- },
1479
- _isValidFileDrag: function(e){
1480
- // e.dataTransfer currently causing IE errors
1481
- // IE9 does NOT support file API, so drag-and-drop is not possible
1482
- if (qq.ie() && !qq.ie10()) return false;
2487
+ if (options.cors.expected && options.cors.sendCredentials) {
2488
+ xhr.withCredentials = true;
2489
+ }
1483
2490
 
1484
- var dt = e.dataTransfer,
1485
- // do not check dt.types.contains in webkit, because it crashes safari 4
1486
- isSafari = qq.safari();
2491
+ setHeaders(id);
1487
2492
 
1488
- // dt.effectAllowed is none in Safari 5
1489
- // dt.types.contains check is for firefox
1490
- var effectTest = qq.ie10() ? true : dt.effectAllowed != 'none';
1491
- return dt && effectTest && (dt.files || (!isSafari && dt.types.contains && dt.types.contains('Files')));
2493
+ log('Sending ' + method + " request for " + id);
2494
+ if (!shouldParamsBeInQueryString && params) {
2495
+ xhr.send(qq.obj2url(params, ""));
2496
+ }
2497
+ else {
2498
+ xhr.send();
2499
+ }
1492
2500
  }
1493
- };
1494
- /**
1495
- * Class for uploading files, uploading itself is handled by child classes
1496
- */
1497
- qq.UploadHandlerAbstract = function(o){
1498
- // Default options, can be overridden by the user
1499
- this._options = {
1500
- debug: false,
1501
- endpoint: '/upload.php',
1502
- // maximum number of concurrent uploads
1503
- maxConnections: 999,
1504
- log: function(str, level) {},
1505
- onProgress: function(id, fileName, loaded, total){},
1506
- onComplete: function(id, fileName, response, xhr){},
1507
- onCancel: function(id, fileName){},
1508
- onUpload: function(id, fileName, xhr){},
1509
- onAutoRetry: function(id, fileName, response, xhr){}
1510
-
1511
- };
1512
- qq.extend(this._options, o);
1513
-
1514
- this._queue = [];
1515
- // params for files in queue
1516
- this._params = [];
1517
-
1518
- this.log = this._options.log;
1519
- };
1520
- qq.UploadHandlerAbstract.prototype = {
1521
- /**
1522
- * Adds file or file input to the queue
1523
- * @returns id
1524
- **/
1525
- add: function(file){},
1526
- /**
1527
- * Sends the file identified by id and additional query params to the server
1528
- */
1529
- upload: function(id, params){
1530
- var len = this._queue.push(id);
1531
2501
 
1532
- var copy = {};
1533
- qq.extend(copy, params);
1534
- this._params[id] = copy;
2502
+ function createUrl(id, params) {
2503
+ var endpoint = options.endpointStore.getEndpoint(id),
2504
+ addToPath = requestState[id].addToPath;
1535
2505
 
1536
- // if too many active uploads, wait...
1537
- if (len <= this._options.maxConnections){
1538
- this._upload(id, this._params[id]);
2506
+ if (addToPath !== undefined) {
2507
+ endpoint += "/" + addToPath;
1539
2508
  }
1540
- },
1541
- retry: function(id) {
1542
- var i = qq.indexOf(this._queue, id);
1543
- if (i >= 0) {
1544
- this._upload(id, this._params[id]);
2509
+
2510
+ if (shouldParamsBeInQueryString && params) {
2511
+ return qq.obj2url(params, endpoint);
1545
2512
  }
1546
2513
  else {
1547
- this.upload(id, this._params[id]);
1548
- }
1549
- },
1550
- /**
1551
- * Cancels file upload by id
1552
- */
1553
- cancel: function(id){
1554
- this.log('Cancelling ' + id);
1555
- this._cancel(id);
1556
- this._dequeue(id);
1557
- },
1558
- /**
1559
- * Cancells all uploads
1560
- */
1561
- cancelAll: function(){
1562
- for (var i=0; i<this._queue.length; i++){
1563
- this._cancel(this._queue[i]);
2514
+ return endpoint;
1564
2515
  }
1565
- this._queue = [];
1566
- },
1567
- /**
1568
- * Returns name of the file identified by id
1569
- */
1570
- getName: function(id){},
1571
- /**
1572
- * Returns size of the file identified by id
1573
- */
1574
- getSize: function(id){},
1575
- /**
1576
- * Returns id of files being uploaded or
1577
- * waiting for their turn
1578
- */
1579
- getQueue: function(){
1580
- return this._queue;
1581
- },
1582
- reset: function() {
1583
- this.log('Resetting upload handler');
1584
- this._queue = [];
1585
- this._params = [];
1586
- },
1587
- /**
1588
- * Actual upload method
1589
- */
1590
- _upload: function(id){},
1591
- /**
1592
- * Actual cancel method
1593
- */
1594
- _cancel: function(id){},
1595
- /**
1596
- * Removes element from queue, starts upload of next
1597
- */
1598
- _dequeue: function(id){
1599
- var i = qq.indexOf(this._queue, id);
1600
- this._queue.splice(i, 1);
1601
-
1602
- var max = this._options.maxConnections;
2516
+ }
1603
2517
 
1604
- if (this._queue.length >= max && i < max){
1605
- var nextId = this._queue[max-1];
1606
- this._upload(nextId, this._params[nextId]);
1607
- }
1608
- },
1609
- /**
1610
- * Determine if the file exists.
1611
- */
1612
- isValid: function(id) {}
1613
- };
1614
- /**
1615
- * Class for uploading files using form and iframe
1616
- * @inherits qq.UploadHandlerAbstract
1617
- */
1618
- qq.UploadHandlerForm = function(o){
1619
- qq.UploadHandlerAbstract.apply(this, arguments);
2518
+ function getReadyStateChangeHandler(id) {
2519
+ var xhr = requestState[id].xhr;
1620
2520
 
1621
- this._inputs = {};
1622
- this._detach_load_events = {};
1623
- };
1624
- // @inherits qq.UploadHandlerAbstract
1625
- qq.extend(qq.UploadHandlerForm.prototype, qq.UploadHandlerAbstract.prototype);
2521
+ return function() {
2522
+ if (xhr.readyState === 4) {
2523
+ onComplete(id, xhr);
2524
+ }
2525
+ };
2526
+ }
1626
2527
 
1627
- qq.extend(qq.UploadHandlerForm.prototype, {
1628
- add: function(fileInput){
1629
- fileInput.setAttribute('name', this._options.inputName);
1630
- var id = 'qq-upload-handler-iframe' + qq.getUniqueId();
2528
+ function setHeaders(id) {
2529
+ var xhr = requestState[id].xhr,
2530
+ customHeaders = options.customHeaders;
1631
2531
 
1632
- this._inputs[id] = fileInput;
2532
+ xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
2533
+ xhr.setRequestHeader("Cache-Control", "no-cache");
1633
2534
 
1634
- // remove file input from DOM
1635
- if (fileInput.parentNode){
1636
- qq(fileInput).remove();
1637
- }
2535
+ qq.each(customHeaders, function(name, val) {
2536
+ xhr.setRequestHeader(name, val);
2537
+ });
2538
+ }
1638
2539
 
1639
- return id;
1640
- },
1641
- getName: function(id){
1642
- // get input value and remove path to normalize
1643
- return this._inputs[id].value.replace(/.*(\/|\\)/, "");
1644
- },
1645
- isValid: function(id) {
1646
- return this._inputs[id] !== undefined;
1647
- },
1648
- reset: function() {
1649
- qq.UploadHandlerAbstract.prototype.reset.apply(this, arguments);
1650
- this._inputs = {};
1651
- this._detach_load_events = {};
1652
- },
1653
- _cancel: function(id){
1654
- this._options.onCancel(id, this.getName(id));
2540
+ function cancelRequest(id) {
2541
+ var xhr = requestState[id].xhr,
2542
+ method = getMethod();
1655
2543
 
1656
- delete this._inputs[id];
1657
- delete this._detach_load_events[id];
2544
+ if (xhr) {
2545
+ xhr.onreadystatechange = null;
2546
+ xhr.abort();
2547
+ dequeue(id);
1658
2548
 
1659
- var iframe = document.getElementById(id);
1660
- if (iframe){
1661
- // to cancel request set src to something else
1662
- // we use src="javascript:false;" because it doesn't
1663
- // trigger ie6 prompt on https
1664
- iframe.setAttribute('src', 'javascript:false;');
2549
+ log('Cancelled ' + method + " for " + id);
2550
+ options.onCancel(id);
1665
2551
 
1666
- qq(iframe).remove();
2552
+ return true;
1667
2553
  }
1668
- },
1669
- _upload: function(id, params){
1670
- this._options.onUpload(id, this.getName(id), false);
1671
- var input = this._inputs[id];
1672
2554
 
1673
- if (!input){
1674
- throw new Error('file with passed id was not added, or already uploaded or cancelled');
1675
- }
2555
+ return false;
2556
+ }
1676
2557
 
1677
- var fileName = this.getName(id);
1678
- params[this._options.inputName] = fileName;
2558
+ function isResponseSuccessful(responseCode) {
2559
+ return qq.indexOf(options.successfulResponseCodes, responseCode) >= 0;
2560
+ }
1679
2561
 
1680
- var iframe = this._createIframe(id);
1681
- var form = this._createForm(iframe, params);
1682
- form.appendChild(input);
2562
+ function getMethod() {
2563
+ if (options.demoMode) {
2564
+ return "GET";
2565
+ }
1683
2566
 
1684
- var self = this;
1685
- this._attachLoadEvent(iframe, function(){
1686
- self.log('iframe loaded');
2567
+ return options.method;
2568
+ }
1687
2569
 
1688
- var response = self._getIframeContentJSON(iframe);
1689
2570
 
1690
- // timeout added to fix busy state in FF3.6
1691
- setTimeout(function(){
1692
- self._detach_load_events[id]();
1693
- delete self._detach_load_events[id];
1694
- qq(iframe).remove();
1695
- }, 1);
2571
+ return {
2572
+ send: function(id, addToPath) {
2573
+ requestState[id] = {
2574
+ addToPath: addToPath
2575
+ };
1696
2576
 
1697
- if (!response.success) {
1698
- if (self._options.onAutoRetry(id, fileName, response)) {
1699
- return;
1700
- }
2577
+ var len = queue.push(id);
2578
+
2579
+ // if too many active connections, wait...
2580
+ if (len <= options.maxConnections){
2581
+ sendRequest(id);
1701
2582
  }
1702
- self._options.onComplete(id, fileName, response);
1703
- self._dequeue(id);
1704
- });
2583
+ },
2584
+ cancel: function(id) {
2585
+ return cancelRequest(id);
2586
+ }
2587
+ };
2588
+ };
2589
+ /** Generic class for sending non-upload ajax requests and handling the associated responses **/
2590
+ /*globals qq, XMLHttpRequest*/
2591
+ qq.DeleteFileAjaxRequestor = function(o) {
2592
+ "use strict";
1705
2593
 
1706
- this.log('Sending upload request for ' + id);
1707
- form.submit();
1708
- qq(form).remove();
2594
+ var requestor,
2595
+ options = {
2596
+ endpointStore: {},
2597
+ maxConnections: 3,
2598
+ customHeaders: {},
2599
+ paramsStore: {},
2600
+ demoMode: false,
2601
+ cors: {
2602
+ expected: false,
2603
+ sendCredentials: false
2604
+ },
2605
+ log: function(str, level) {},
2606
+ onDelete: function(id) {},
2607
+ onDeleteComplete: function(id, xhr, isError) {}
2608
+ };
1709
2609
 
1710
- return id;
1711
- },
1712
- _attachLoadEvent: function(iframe, callback){
1713
- var self = this;
1714
- this._detach_load_events[iframe.id] = qq(iframe).attach('load', function(){
1715
- self.log('Received response for ' + iframe.id);
2610
+ qq.extend(options, o);
2611
+
2612
+ requestor = new qq.AjaxRequestor({
2613
+ method: 'DELETE',
2614
+ endpointStore: options.endpointStore,
2615
+ paramsStore: options.paramsStore,
2616
+ maxConnections: options.maxConnections,
2617
+ customHeaders: options.customHeaders,
2618
+ successfulResponseCodes: [200, 202, 204],
2619
+ demoMode: options.demoMode,
2620
+ log: options.log,
2621
+ onSend: options.onDelete,
2622
+ onComplete: options.onDeleteComplete
2623
+ });
1716
2624
 
1717
- // when we remove iframe from dom
1718
- // the request stops, but in IE load
1719
- // event fires
1720
- if (!iframe.parentNode){
1721
- return;
2625
+
2626
+ return {
2627
+ sendDelete: function(id, uuid) {
2628
+ requestor.send(id, uuid);
2629
+ options.log("Submitted delete file request for " + id);
2630
+ }
2631
+ };
2632
+ };
2633
+ qq.WindowReceiveMessage = function(o) {
2634
+ var options = {
2635
+ log: function(message, level) {}
2636
+ },
2637
+ callbackWrapperDetachers = {};
2638
+
2639
+ qq.extend(options, o);
2640
+
2641
+ return {
2642
+ receiveMessage : function(id, callback) {
2643
+ var onMessageCallbackWrapper = function(event) {
2644
+ callback(event.data);
2645
+ };
2646
+
2647
+ if (window.postMessage) {
2648
+ callbackWrapperDetachers[id] = qq(window).attach("message", onMessageCallbackWrapper);
2649
+ }
2650
+ else {
2651
+ log("iframe message passing not supported in this browser!", "error");
1722
2652
  }
2653
+ },
1723
2654
 
1724
- try {
1725
- // fixing Opera 10.53
1726
- if (iframe.contentDocument &&
1727
- iframe.contentDocument.body &&
1728
- iframe.contentDocument.body.innerHTML == "false"){
1729
- // In Opera event is fired second time
1730
- // when body.innerHTML changed from false
1731
- // to server response approx. after 1 sec
1732
- // when we upload file with iframe
1733
- return;
2655
+ stopReceivingMessages : function(id) {
2656
+ if (window.postMessage) {
2657
+ var detacher = callbackWrapperDetachers[id];
2658
+ if (detacher) {
2659
+ detacher();
1734
2660
  }
1735
2661
  }
1736
- catch (error) {
1737
- //IE may throw an "access is denied" error when attempting to access contentDocument on the iframe in some cases
1738
- self.log('Error when attempting to access iframe during handling of upload response (' + error + ")", 'error');
2662
+ }
2663
+ };
2664
+ };
2665
+ /**
2666
+ * Class for uploading files, uploading itself is handled by child classes
2667
+ */
2668
+ /*globals qq*/
2669
+ qq.UploadHandler = function(o) {
2670
+ "use strict";
2671
+
2672
+ var queue = [],
2673
+ options, log, dequeue, handlerImpl;
2674
+
2675
+ // Default options, can be overridden by the user
2676
+ options = {
2677
+ debug: false,
2678
+ forceMultipart: true,
2679
+ paramsInBody: false,
2680
+ paramsStore: {},
2681
+ endpointStore: {},
2682
+ cors: {
2683
+ expected: false,
2684
+ sendCredentials: false
2685
+ },
2686
+ maxConnections: 3, // maximum number of concurrent uploads
2687
+ uuidParamName: 'qquuid',
2688
+ totalFileSizeParamName: 'qqtotalfilesize',
2689
+ chunking: {
2690
+ enabled: false,
2691
+ partSize: 2000000, //bytes
2692
+ paramNames: {
2693
+ partIndex: 'qqpartindex',
2694
+ partByteOffset: 'qqpartbyteoffset',
2695
+ chunkSize: 'qqchunksize',
2696
+ totalParts: 'qqtotalparts',
2697
+ filename: 'qqfilename'
2698
+ }
2699
+ },
2700
+ resume: {
2701
+ enabled: false,
2702
+ id: null,
2703
+ cookiesExpireIn: 7, //days
2704
+ paramNames: {
2705
+ resuming: "qqresume"
2706
+ }
2707
+ },
2708
+ blobs: {
2709
+ paramNames: {
2710
+ name: 'qqblobname'
2711
+ }
2712
+ },
2713
+ log: function(str, level) {},
2714
+ onProgress: function(id, fileName, loaded, total){},
2715
+ onComplete: function(id, fileName, response, xhr){},
2716
+ onCancel: function(id, fileName){},
2717
+ onUpload: function(id, fileName){},
2718
+ onUploadChunk: function(id, fileName, chunkData){},
2719
+ onAutoRetry: function(id, fileName, response, xhr){},
2720
+ onResume: function(id, fileName, chunkData){}
2721
+
2722
+ };
2723
+ qq.extend(options, o);
2724
+
2725
+ log = options.log;
2726
+
2727
+ /**
2728
+ * Removes element from queue, starts upload of next
2729
+ */
2730
+ dequeue = function(id) {
2731
+ var i = qq.indexOf(queue, id),
2732
+ max = options.maxConnections,
2733
+ nextId;
2734
+
2735
+ if (i >= 0) {
2736
+ queue.splice(i, 1);
2737
+
2738
+ if (queue.length >= max && i < max){
2739
+ nextId = queue[max-1];
2740
+ handlerImpl.upload(nextId);
2741
+ }
2742
+ }
2743
+ };
2744
+
2745
+ if (qq.isXhrUploadSupported()) {
2746
+ handlerImpl = new qq.UploadHandlerXhr(options, dequeue, log);
2747
+ }
2748
+ else {
2749
+ handlerImpl = new qq.UploadHandlerForm(options, dequeue, log);
2750
+ }
2751
+
2752
+
2753
+ return {
2754
+ /**
2755
+ * Adds file or file input to the queue
2756
+ * @returns id
2757
+ **/
2758
+ add: function(file){
2759
+ return handlerImpl.add(file);
2760
+ },
2761
+ /**
2762
+ * Sends the file identified by id
2763
+ */
2764
+ upload: function(id){
2765
+ var len = queue.push(id);
2766
+
2767
+ // if too many active uploads, wait...
2768
+ if (len <= options.maxConnections){
2769
+ return handlerImpl.upload(id);
2770
+ }
2771
+ },
2772
+ retry: function(id) {
2773
+ var i = qq.indexOf(queue, id);
2774
+ if (i >= 0) {
2775
+ return handlerImpl.upload(id, true);
2776
+ }
2777
+ else {
2778
+ return this.upload(id);
1739
2779
  }
2780
+ },
2781
+ /**
2782
+ * Cancels file upload by id
2783
+ */
2784
+ cancel: function(id) {
2785
+ log('Cancelling ' + id);
2786
+ options.paramsStore.remove(id);
2787
+ handlerImpl.cancel(id);
2788
+ dequeue(id);
2789
+ },
2790
+ /**
2791
+ * Cancels all queued or in-progress uploads
2792
+ */
2793
+ cancelAll: function() {
2794
+ var self = this,
2795
+ queueCopy = [];
2796
+
2797
+ qq.extend(queueCopy, queue);
2798
+ qq.each(queueCopy, function(idx, fileId) {
2799
+ self.cancel(fileId);
2800
+ });
1740
2801
 
1741
- callback();
2802
+ queue = [];
2803
+ },
2804
+ /**
2805
+ * Returns name of the file identified by id
2806
+ */
2807
+ getName: function(id){
2808
+ return handlerImpl.getName(id);
2809
+ },
2810
+ /**
2811
+ * Returns size of the file identified by id
2812
+ */
2813
+ getSize: function(id){
2814
+ if (handlerImpl.getSize) {
2815
+ return handlerImpl.getSize(id);
2816
+ }
2817
+ },
2818
+ getFile: function(id) {
2819
+ if (handlerImpl.getFile) {
2820
+ return handlerImpl.getFile(id);
2821
+ }
2822
+ },
2823
+ /**
2824
+ * Returns id of files being uploaded or
2825
+ * waiting for their turn
2826
+ */
2827
+ getQueue: function(){
2828
+ return queue;
2829
+ },
2830
+ reset: function() {
2831
+ log('Resetting upload handler');
2832
+ queue = [];
2833
+ handlerImpl.reset();
2834
+ },
2835
+ getUuid: function(id) {
2836
+ return handlerImpl.getUuid(id);
2837
+ },
2838
+ /**
2839
+ * Determine if the file exists.
2840
+ */
2841
+ isValid: function(id) {
2842
+ return handlerImpl.isValid(id);
2843
+ },
2844
+ getResumableFilesData: function() {
2845
+ if (handlerImpl.getResumableFilesData) {
2846
+ return handlerImpl.getResumableFilesData();
2847
+ }
2848
+ return [];
2849
+ }
2850
+ };
2851
+ };
2852
+ /*globals qq, document, setTimeout*/
2853
+ /*globals clearTimeout*/
2854
+ qq.UploadHandlerForm = function(o, uploadCompleteCallback, logCallback) {
2855
+ "use strict";
2856
+
2857
+ var options = o,
2858
+ inputs = [],
2859
+ uuids = [],
2860
+ detachLoadEvents = {},
2861
+ postMessageCallbackTimers = {},
2862
+ uploadComplete = uploadCompleteCallback,
2863
+ log = logCallback,
2864
+ corsMessageReceiver = new qq.WindowReceiveMessage({log: log}),
2865
+ onloadCallbacks = {},
2866
+ api;
2867
+
2868
+
2869
+ function detachLoadEvent(id) {
2870
+ if (detachLoadEvents[id] !== undefined) {
2871
+ detachLoadEvents[id]();
2872
+ delete detachLoadEvents[id];
2873
+ }
2874
+ }
2875
+
2876
+ function registerPostMessageCallback(iframe, callback) {
2877
+ var id = iframe.id;
2878
+
2879
+ onloadCallbacks[uuids[id]] = callback;
2880
+
2881
+ detachLoadEvents[id] = qq(iframe).attach('load', function() {
2882
+ if (inputs[id]) {
2883
+ log("Received iframe load event for CORS upload request (file id " + id + ")");
2884
+
2885
+ postMessageCallbackTimers[id] = setTimeout(function() {
2886
+ var errorMessage = "No valid message received from loaded iframe for file id " + id;
2887
+ log(errorMessage, "error");
2888
+ callback({
2889
+ error: errorMessage
2890
+ });
2891
+ }, 1000);
2892
+ }
1742
2893
  });
1743
- },
2894
+
2895
+ corsMessageReceiver.receiveMessage(id, function(message) {
2896
+ log("Received the following window message: '" + message + "'");
2897
+ var response = qq.parseJson(message),
2898
+ uuid = response.uuid,
2899
+ onloadCallback;
2900
+
2901
+ if (uuid && onloadCallbacks[uuid]) {
2902
+ clearTimeout(postMessageCallbackTimers[id]);
2903
+ delete postMessageCallbackTimers[id];
2904
+
2905
+ detachLoadEvent(id);
2906
+
2907
+ onloadCallback = onloadCallbacks[uuid];
2908
+
2909
+ delete onloadCallbacks[uuid];
2910
+ corsMessageReceiver.stopReceivingMessages(id);
2911
+ onloadCallback(response);
2912
+ }
2913
+ else if (!uuid) {
2914
+ log("'" + message + "' does not contain a UUID - ignoring.");
2915
+ }
2916
+ });
2917
+ }
2918
+
2919
+ function attachLoadEvent(iframe, callback) {
2920
+ /*jslint eqeq: true*/
2921
+
2922
+ if (options.cors.expected) {
2923
+ registerPostMessageCallback(iframe, callback);
2924
+ }
2925
+ else {
2926
+ detachLoadEvents[iframe.id] = qq(iframe).attach('load', function(){
2927
+ log('Received response for ' + iframe.id);
2928
+
2929
+ // when we remove iframe from dom
2930
+ // the request stops, but in IE load
2931
+ // event fires
2932
+ if (!iframe.parentNode){
2933
+ return;
2934
+ }
2935
+
2936
+ try {
2937
+ // fixing Opera 10.53
2938
+ if (iframe.contentDocument &&
2939
+ iframe.contentDocument.body &&
2940
+ iframe.contentDocument.body.innerHTML == "false"){
2941
+ // In Opera event is fired second time
2942
+ // when body.innerHTML changed from false
2943
+ // to server response approx. after 1 sec
2944
+ // when we upload file with iframe
2945
+ return;
2946
+ }
2947
+ }
2948
+ catch (error) {
2949
+ //IE may throw an "access is denied" error when attempting to access contentDocument on the iframe in some cases
2950
+ log('Error when attempting to access iframe during handling of upload response (' + error + ")", 'error');
2951
+ }
2952
+
2953
+ callback();
2954
+ });
2955
+ }
2956
+ }
2957
+
1744
2958
  /**
1745
2959
  * Returns json object received by iframe from server.
1746
2960
  */
1747
- _getIframeContentJSON: function(iframe){
2961
+ function getIframeContentJson(iframe) {
2962
+ /*jshint evil: true*/
2963
+
2964
+ var response;
2965
+
1748
2966
  //IE may throw an "access is denied" error when attempting to access contentDocument on the iframe in some cases
1749
2967
  try {
1750
2968
  // iframe.contentWindow.document - for IE<7
1751
- var doc = iframe.contentDocument ? iframe.contentDocument: iframe.contentWindow.document,
1752
- response;
2969
+ var doc = iframe.contentDocument || iframe.contentWindow.document,
2970
+ innerHTML = doc.body.innerHTML;
1753
2971
 
1754
- var innerHTML = doc.body.innerHTML;
1755
- this.log("converting iframe's innerHTML to JSON");
1756
- this.log("innerHTML = " + innerHTML);
2972
+ log("converting iframe's innerHTML to JSON");
2973
+ log("innerHTML = " + innerHTML);
1757
2974
  //plain text response may be wrapped in <pre> tag
1758
2975
  if (innerHTML && innerHTML.match(/^<pre/i)) {
1759
2976
  innerHTML = doc.body.firstChild.firstChild.nodeValue;
1760
2977
  }
1761
- response = eval("(" + innerHTML + ")");
2978
+
2979
+ response = qq.parseJson(innerHTML);
1762
2980
  } catch(error){
1763
- this.log('Error when attempting to parse form upload response (' + error + ")", 'error');
2981
+ log('Error when attempting to parse form upload response (' + error + ")", 'error');
1764
2982
  response = {success: false};
1765
2983
  }
1766
2984
 
1767
2985
  return response;
1768
- },
2986
+ }
2987
+
1769
2988
  /**
1770
2989
  * Creates iframe with unique name
1771
2990
  */
1772
- _createIframe: function(id){
2991
+ function createIframe(id){
1773
2992
  // We can't use following code as the name attribute
1774
2993
  // won't be properly registered in IE6, and new window
1775
2994
  // on form submit will open
@@ -1777,7 +2996,6 @@ qq.extend(qq.UploadHandlerForm.prototype, {
1777
2996
  // iframe.setAttribute('name', id);
1778
2997
 
1779
2998
  var iframe = qq.toElement('<iframe src="javascript:false;" name="' + id + '" />');
1780
- // src="javascript:false;" removes ie6 prompt on https
1781
2999
 
1782
3000
  iframe.setAttribute('id', id);
1783
3001
 
@@ -1785,194 +3003,759 @@ qq.extend(qq.UploadHandlerForm.prototype, {
1785
3003
  document.body.appendChild(iframe);
1786
3004
 
1787
3005
  return iframe;
1788
- },
3006
+ }
3007
+
1789
3008
  /**
1790
3009
  * Creates form, that will be submitted to iframe
1791
3010
  */
1792
- _createForm: function(iframe, params){
1793
- // We can't use the following code in IE6
1794
- // var form = document.createElement('form');
1795
- // form.setAttribute('method', 'post');
1796
- // form.setAttribute('enctype', 'multipart/form-data');
1797
- // Because in this case file won't be attached to request
1798
- var protocol = this._options.demoMode ? "GET" : "POST"
1799
- var form = qq.toElement('<form method="' + protocol + '" enctype="multipart/form-data"></form>');
1800
-
1801
- var queryString = qq.obj2url(params, this._options.endpoint);
1802
-
1803
- form.setAttribute('action', queryString);
3011
+ function createForm(id, iframe){
3012
+ var params = options.paramsStore.getParams(id),
3013
+ protocol = options.demoMode ? "GET" : "POST",
3014
+ form = qq.toElement('<form method="' + protocol + '" enctype="multipart/form-data"></form>'),
3015
+ endpoint = options.endpointStore.getEndpoint(id),
3016
+ url = endpoint;
3017
+
3018
+ params[options.uuidParamName] = uuids[id];
3019
+
3020
+ if (!options.paramsInBody) {
3021
+ url = qq.obj2url(params, endpoint);
3022
+ }
3023
+ else {
3024
+ qq.obj2Inputs(params, form);
3025
+ }
3026
+
3027
+ form.setAttribute('action', url);
1804
3028
  form.setAttribute('target', iframe.name);
1805
3029
  form.style.display = 'none';
1806
3030
  document.body.appendChild(form);
1807
3031
 
1808
3032
  return form;
1809
3033
  }
1810
- });
1811
- /**
1812
- * Class for uploading files using xhr
1813
- * @inherits qq.UploadHandlerAbstract
1814
- */
1815
- qq.UploadHandlerXhr = function(o){
1816
- qq.UploadHandlerAbstract.apply(this, arguments);
1817
3034
 
1818
- this._files = [];
1819
- this._xhrs = [];
1820
3035
 
1821
- // current loaded size in bytes for each file
1822
- this._loaded = [];
1823
- };
3036
+ api = {
3037
+ add: function(fileInput) {
3038
+ fileInput.setAttribute('name', options.inputName);
1824
3039
 
1825
- // static method
1826
- qq.UploadHandlerXhr.isSupported = function(){
1827
- var input = document.createElement('input');
1828
- input.type = 'file';
3040
+ var id = inputs.push(fileInput) - 1;
3041
+ uuids[id] = qq.getUniqueId();
1829
3042
 
1830
- return (
1831
- 'multiple' in input &&
1832
- typeof File != "undefined" &&
1833
- typeof FormData != "undefined" &&
1834
- typeof (new XMLHttpRequest()).upload != "undefined" );
3043
+ // remove file input from DOM
3044
+ if (fileInput.parentNode){
3045
+ qq(fileInput).remove();
3046
+ }
3047
+
3048
+ return id;
3049
+ },
3050
+ getName: function(id) {
3051
+ /*jslint regexp: true*/
3052
+
3053
+ // get input value and remove path to normalize
3054
+ return inputs[id].value.replace(/.*(\/|\\)/, "");
3055
+ },
3056
+ isValid: function(id) {
3057
+ return inputs[id] !== undefined;
3058
+ },
3059
+ reset: function() {
3060
+ qq.UploadHandler.prototype.reset.apply(this, arguments);
3061
+ inputs = [];
3062
+ uuids = [];
3063
+ detachLoadEvents = {};
3064
+ },
3065
+ getUuid: function(id) {
3066
+ return uuids[id];
3067
+ },
3068
+ cancel: function(id) {
3069
+ options.onCancel(id, this.getName(id));
3070
+
3071
+ delete inputs[id];
3072
+ delete uuids[id];
3073
+ delete detachLoadEvents[id];
3074
+
3075
+ if (options.cors.expected) {
3076
+ clearTimeout(postMessageCallbackTimers[id]);
3077
+ delete postMessageCallbackTimers[id];
3078
+ corsMessageReceiver.stopReceivingMessages(id);
3079
+ }
3080
+
3081
+ var iframe = document.getElementById(id);
3082
+ if (iframe) {
3083
+ // to cancel request set src to something else
3084
+ // we use src="javascript:false;" because it doesn't
3085
+ // trigger ie6 prompt on https
3086
+ iframe.setAttribute('src', 'java' + String.fromCharCode(115) + 'cript:false;'); //deal with "JSLint: javascript URL" warning, which apparently cannot be turned off
3087
+
3088
+ qq(iframe).remove();
3089
+ }
3090
+ },
3091
+ upload: function(id){
3092
+ var input = inputs[id],
3093
+ fileName = api.getName(id),
3094
+ iframe = createIframe(id),
3095
+ form;
3096
+
3097
+ if (!input){
3098
+ throw new Error('file with passed id was not added, or already uploaded or cancelled');
3099
+ }
3100
+
3101
+ options.onUpload(id, this.getName(id));
3102
+
3103
+ form = createForm(id, iframe);
3104
+ form.appendChild(input);
3105
+
3106
+ attachLoadEvent(iframe, function(responseFromMessage){
3107
+ log('iframe loaded');
3108
+
3109
+ var response = responseFromMessage ? responseFromMessage : getIframeContentJson(iframe);
3110
+
3111
+ detachLoadEvent(id);
3112
+
3113
+ //we can't remove an iframe if the iframe doesn't belong to the same domain
3114
+ if (!options.cors.expected) {
3115
+ qq(iframe).remove();
3116
+ }
3117
+
3118
+ if (!response.success) {
3119
+ if (options.onAutoRetry(id, fileName, response)) {
3120
+ return;
3121
+ }
3122
+ }
3123
+ options.onComplete(id, fileName, response);
3124
+ uploadComplete(id);
3125
+ });
3126
+
3127
+ log('Sending upload request for ' + id);
3128
+ form.submit();
3129
+ qq(form).remove();
3130
+
3131
+ return id;
3132
+ }
3133
+ };
3134
+
3135
+ return api;
1835
3136
  };
3137
+ /*globals qq, File, XMLHttpRequest, FormData, Blob*/
3138
+ qq.UploadHandlerXhr = function(o, uploadCompleteCallback, logCallback) {
3139
+ "use strict";
3140
+
3141
+ var options = o,
3142
+ uploadComplete = uploadCompleteCallback,
3143
+ log = logCallback,
3144
+ fileState = [],
3145
+ cookieItemDelimiter = "|",
3146
+ chunkFiles = options.chunking.enabled && qq.isFileChunkingSupported(),
3147
+ resumeEnabled = options.resume.enabled && chunkFiles && qq.areCookiesEnabled(),
3148
+ resumeId = getResumeId(),
3149
+ multipart = options.forceMultipart || options.paramsInBody,
3150
+ api;
3151
+
3152
+
3153
+ function addChunkingSpecificParams(id, params, chunkData) {
3154
+ var size = api.getSize(id),
3155
+ name = api.getName(id);
3156
+
3157
+ params[options.chunking.paramNames.partIndex] = chunkData.part;
3158
+ params[options.chunking.paramNames.partByteOffset] = chunkData.start;
3159
+ params[options.chunking.paramNames.chunkSize] = chunkData.size;
3160
+ params[options.chunking.paramNames.totalParts] = chunkData.count;
3161
+ params[options.totalFileSizeParamName] = size;
1836
3162
 
1837
- // @inherits qq.UploadHandlerAbstract
1838
- qq.extend(qq.UploadHandlerXhr.prototype, qq.UploadHandlerAbstract.prototype)
3163
+ /**
3164
+ * When a Blob is sent in a multipart request, the filename value in the content-disposition header is either "blob"
3165
+ * or an empty string. So, we will need to include the actual file name as a param in this case.
3166
+ */
3167
+ if (multipart) {
3168
+ params[options.chunking.paramNames.filename] = name;
3169
+ }
3170
+ }
1839
3171
 
1840
- qq.extend(qq.UploadHandlerXhr.prototype, {
1841
- /**
1842
- * Adds file to the queue
1843
- * Returns id to use with upload, cancel
1844
- **/
1845
- add: function(file){
1846
- if (!(file instanceof File)){
1847
- throw new Error('Passed obj in not a File (in qq.UploadHandlerXhr)');
3172
+ function addResumeSpecificParams(params) {
3173
+ params[options.resume.paramNames.resuming] = true;
3174
+ }
3175
+
3176
+ function getChunk(fileOrBlob, startByte, endByte) {
3177
+ if (fileOrBlob.slice) {
3178
+ return fileOrBlob.slice(startByte, endByte);
3179
+ }
3180
+ else if (fileOrBlob.mozSlice) {
3181
+ return fileOrBlob.mozSlice(startByte, endByte);
3182
+ }
3183
+ else if (fileOrBlob.webkitSlice) {
3184
+ return fileOrBlob.webkitSlice(startByte, endByte);
1848
3185
  }
3186
+ }
1849
3187
 
1850
- return this._files.push(file) - 1;
1851
- },
1852
- getName: function(id){
1853
- var file = this._files[id];
1854
- // fix missing name in Safari 4
1855
- //NOTE: fixed missing name firefox 11.0a2 file.fileName is actually undefined
1856
- return (file.fileName !== null && file.fileName !== undefined) ? file.fileName : file.name;
1857
- },
1858
- getSize: function(id){
1859
- var file = this._files[id];
1860
- return file.fileSize != null ? file.fileSize : file.size;
1861
- },
1862
- /**
1863
- * Returns uploaded bytes for file identified by id
1864
- */
1865
- getLoaded: function(id){
1866
- return this._loaded[id] || 0;
1867
- },
1868
- isValid: function(id) {
1869
- return this._files[id] !== undefined;
1870
- },
1871
- reset: function() {
1872
- qq.UploadHandlerAbstract.prototype.reset.apply(this, arguments);
1873
- this._files = [];
1874
- this._xhrs = [];
1875
- this._loaded = [];
1876
- },
1877
- /**
1878
- * Sends the file identified by id and additional query params to the server
1879
- * @param {Object} params name-value string pairs
1880
- */
1881
- _upload: function(id, params){
1882
- this._options.onUpload(id, this.getName(id), true);
3188
+ function getChunkData(id, chunkIndex) {
3189
+ var chunkSize = options.chunking.partSize,
3190
+ fileSize = api.getSize(id),
3191
+ fileOrBlob = fileState[id].file || fileState[id].blobData.blob,
3192
+ startBytes = chunkSize * chunkIndex,
3193
+ endBytes = startBytes+chunkSize >= fileSize ? fileSize : startBytes+chunkSize,
3194
+ totalChunks = getTotalChunks(id);
3195
+
3196
+ return {
3197
+ part: chunkIndex,
3198
+ start: startBytes,
3199
+ end: endBytes,
3200
+ count: totalChunks,
3201
+ blob: getChunk(fileOrBlob, startBytes, endBytes),
3202
+ size: endBytes - startBytes
3203
+ };
3204
+ }
1883
3205
 
1884
- var file = this._files[id],
1885
- name = this.getName(id),
1886
- size = this.getSize(id);
3206
+ function getTotalChunks(id) {
3207
+ var fileSize = api.getSize(id),
3208
+ chunkSize = options.chunking.partSize;
1887
3209
 
1888
- this._loaded[id] = 0;
3210
+ return Math.ceil(fileSize / chunkSize);
3211
+ }
1889
3212
 
1890
- var xhr = this._xhrs[id] = new XMLHttpRequest();
1891
- var self = this;
3213
+ function createXhr(id) {
3214
+ var xhr = new XMLHttpRequest();
1892
3215
 
1893
- xhr.upload.onprogress = function(e){
1894
- if (e.lengthComputable){
1895
- self._loaded[id] = e.loaded;
1896
- self._options.onProgress(id, name, e.loaded, e.total);
3216
+ fileState[id].xhr = xhr;
3217
+
3218
+ return xhr;
3219
+ }
3220
+
3221
+ function setParamsAndGetEntityToSend(params, xhr, fileOrBlob, id) {
3222
+ var formData = new FormData(),
3223
+ method = options.demoMode ? "GET" : "POST",
3224
+ endpoint = options.endpointStore.getEndpoint(id),
3225
+ url = endpoint,
3226
+ name = api.getName(id),
3227
+ size = api.getSize(id),
3228
+ blobData = fileState[id].blobData;
3229
+
3230
+ params[options.uuidParamName] = fileState[id].uuid;
3231
+
3232
+ if (multipart) {
3233
+ params[options.totalFileSizeParamName] = size;
3234
+
3235
+ if (blobData) {
3236
+ /**
3237
+ * When a Blob is sent in a multipart request, the filename value in the content-disposition header is either "blob"
3238
+ * or an empty string. So, we will need to include the actual file name as a param in this case.
3239
+ */
3240
+ params[options.blobs.paramNames.name] = blobData.name;
1897
3241
  }
1898
- };
3242
+ }
1899
3243
 
1900
- xhr.onreadystatechange = function(){
1901
- if (xhr.readyState == 4){
1902
- self._onComplete(id, xhr);
3244
+ //build query string
3245
+ if (!options.paramsInBody) {
3246
+ if (!multipart) {
3247
+ params[options.inputName] = name;
1903
3248
  }
1904
- };
3249
+ url = qq.obj2url(params, endpoint);
3250
+ }
3251
+
3252
+ xhr.open(method, url, true);
3253
+
3254
+ if (options.cors.expected && options.cors.sendCredentials) {
3255
+ xhr.withCredentials = true;
3256
+ }
3257
+
3258
+ if (multipart) {
3259
+ if (options.paramsInBody) {
3260
+ qq.obj2FormData(params, formData);
3261
+ }
3262
+
3263
+ formData.append(options.inputName, fileOrBlob);
3264
+ return formData;
3265
+ }
3266
+
3267
+ return fileOrBlob;
3268
+ }
1905
3269
 
1906
- // build query string
1907
- params = params || {};
1908
- params[this._options.inputName] = name;
1909
- var queryString = qq.obj2url(params, this._options.endpoint);
3270
+ function setHeaders(id, xhr) {
3271
+ var extraHeaders = options.customHeaders,
3272
+ fileOrBlob = fileState[id].file || fileState[id].blobData.blob;
1910
3273
 
1911
- var protocol = this._options.demoMode ? "GET" : "POST";
1912
- xhr.open(protocol, queryString, true);
1913
3274
  xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
1914
- xhr.setRequestHeader("X-File-Name", encodeURIComponent(name));
1915
3275
  xhr.setRequestHeader("Cache-Control", "no-cache");
1916
- if (this._options.forceMultipart) {
1917
- var formData = new FormData();
1918
- formData.append(this._options.inputName, file);
1919
- file = formData;
1920
- } else {
3276
+
3277
+ if (!multipart) {
1921
3278
  xhr.setRequestHeader("Content-Type", "application/octet-stream");
1922
3279
  //NOTE: return mime type in xhr works on chrome 16.0.9 firefox 11.0a2
1923
- xhr.setRequestHeader("X-Mime-Type",file.type );
3280
+ xhr.setRequestHeader("X-Mime-Type", fileOrBlob.type);
1924
3281
  }
1925
- for (key in this._options.customHeaders){
1926
- xhr.setRequestHeader(key, this._options.customHeaders[key]);
3282
+
3283
+ qq.each(extraHeaders, function(name, val) {
3284
+ xhr.setRequestHeader(name, val);
3285
+ });
3286
+ }
3287
+
3288
+ function handleCompletedItem(id, response, xhr) {
3289
+ var name = api.getName(id),
3290
+ size = api.getSize(id);
3291
+
3292
+ fileState[id].attemptingResume = false;
3293
+
3294
+ options.onProgress(id, name, size, size);
3295
+
3296
+ options.onComplete(id, name, response, xhr);
3297
+ delete fileState[id].xhr;
3298
+ uploadComplete(id);
3299
+ }
3300
+
3301
+ function uploadNextChunk(id) {
3302
+ var chunkIdx = fileState[id].remainingChunkIdxs[0],
3303
+ chunkData = getChunkData(id, chunkIdx),
3304
+ xhr = createXhr(id),
3305
+ size = api.getSize(id),
3306
+ name = api.getName(id),
3307
+ toSend, params;
3308
+
3309
+ if (fileState[id].loaded === undefined) {
3310
+ fileState[id].loaded = 0;
3311
+ }
3312
+
3313
+ if (resumeEnabled && fileState[id].file) {
3314
+ persistChunkData(id, chunkData);
3315
+ }
3316
+
3317
+ xhr.onreadystatechange = getReadyStateChangeHandler(id, xhr);
3318
+
3319
+ xhr.upload.onprogress = function(e) {
3320
+ if (e.lengthComputable) {
3321
+ var totalLoaded = e.loaded + fileState[id].loaded,
3322
+ estTotalRequestsSize = calcAllRequestsSizeForChunkedUpload(id, chunkIdx, e.total);
3323
+
3324
+ options.onProgress(id, name, totalLoaded, estTotalRequestsSize);
3325
+ }
1927
3326
  };
1928
3327
 
1929
- this.log('Sending upload request for ' + id);
1930
- xhr.send(file);
1931
- },
1932
- _onComplete: function(id, xhr){
1933
- "use strict";
1934
- // the request was aborted/cancelled
1935
- if (!this._files[id]) { return; }
3328
+ options.onUploadChunk(id, name, getChunkDataForCallback(chunkData));
1936
3329
 
1937
- var name = this.getName(id);
1938
- var size = this.getSize(id);
1939
- var response; //the parsed JSON response from the server, or the empty object if parsing failed.
3330
+ params = options.paramsStore.getParams(id);
3331
+ addChunkingSpecificParams(id, params, chunkData);
1940
3332
 
1941
- this._options.onProgress(id, name, size, size);
3333
+ if (fileState[id].attemptingResume) {
3334
+ addResumeSpecificParams(params);
3335
+ }
1942
3336
 
1943
- this.log("xhr - server response received for " + id);
1944
- this.log("responseText = " + xhr.responseText);
3337
+ toSend = setParamsAndGetEntityToSend(params, xhr, chunkData.blob, id);
3338
+ setHeaders(id, xhr);
1945
3339
 
1946
- try {
1947
- if (typeof JSON.parse === "function") {
1948
- response = JSON.parse(xhr.responseText);
1949
- } else {
1950
- response = eval("(" + xhr.responseText + ")");
3340
+ log('Sending chunked upload request for item ' + id + ": bytes " + (chunkData.start+1) + "-" + chunkData.end + " of " + size);
3341
+ xhr.send(toSend);
3342
+ }
3343
+
3344
+ function calcAllRequestsSizeForChunkedUpload(id, chunkIdx, requestSize) {
3345
+ var chunkData = getChunkData(id, chunkIdx),
3346
+ blobSize = chunkData.size,
3347
+ overhead = requestSize - blobSize,
3348
+ size = api.getSize(id),
3349
+ chunkCount = chunkData.count,
3350
+ initialRequestOverhead = fileState[id].initialRequestOverhead,
3351
+ overheadDiff = overhead - initialRequestOverhead;
3352
+
3353
+ fileState[id].lastRequestOverhead = overhead;
3354
+
3355
+ if (chunkIdx === 0) {
3356
+ fileState[id].lastChunkIdxProgress = 0;
3357
+ fileState[id].initialRequestOverhead = overhead;
3358
+ fileState[id].estTotalRequestsSize = size + (chunkCount * overhead);
3359
+ }
3360
+ else if (fileState[id].lastChunkIdxProgress !== chunkIdx) {
3361
+ fileState[id].lastChunkIdxProgress = chunkIdx;
3362
+ fileState[id].estTotalRequestsSize += overheadDiff;
3363
+ }
3364
+
3365
+ return fileState[id].estTotalRequestsSize;
3366
+ }
3367
+
3368
+ function getLastRequestOverhead(id) {
3369
+ if (multipart) {
3370
+ return fileState[id].lastRequestOverhead;
3371
+ }
3372
+ else {
3373
+ return 0;
3374
+ }
3375
+ }
3376
+
3377
+ function handleSuccessfullyCompletedChunk(id, response, xhr) {
3378
+ var chunkIdx = fileState[id].remainingChunkIdxs.shift(),
3379
+ chunkData = getChunkData(id, chunkIdx);
3380
+
3381
+ fileState[id].attemptingResume = false;
3382
+ fileState[id].loaded += chunkData.size + getLastRequestOverhead(id);
3383
+
3384
+ if (fileState[id].remainingChunkIdxs.length > 0) {
3385
+ uploadNextChunk(id);
3386
+ }
3387
+ else {
3388
+ if (resumeEnabled) {
3389
+ deletePersistedChunkData(id);
1951
3390
  }
1952
- } catch(error){
1953
- this.log('Error when attempting to parse xhr response text (' + error + ')', 'error');
3391
+
3392
+ handleCompletedItem(id, response, xhr);
3393
+ }
3394
+ }
3395
+
3396
+ function isErrorResponse(xhr, response) {
3397
+ return xhr.status !== 200 || !response.success || response.reset;
3398
+ }
3399
+
3400
+ function parseResponse(xhr) {
3401
+ var response;
3402
+
3403
+ try {
3404
+ response = qq.parseJson(xhr.responseText);
3405
+ }
3406
+ catch(error) {
3407
+ log('Error when attempting to parse xhr response text (' + error + ')', 'error');
1954
3408
  response = {};
1955
3409
  }
1956
3410
 
1957
- if (xhr.status !== 200 || !response.success){
1958
- if (this._options.onAutoRetry(id, name, response, xhr)) {
1959
- return;
3411
+ return response;
3412
+ }
3413
+
3414
+ function handleResetResponse(id) {
3415
+ log('Server has ordered chunking effort to be restarted on next attempt for item ID ' + id, 'error');
3416
+
3417
+ if (resumeEnabled) {
3418
+ deletePersistedChunkData(id);
3419
+ fileState[id].attemptingResume = false;
3420
+ }
3421
+
3422
+ fileState[id].remainingChunkIdxs = [];
3423
+ delete fileState[id].loaded;
3424
+ delete fileState[id].estTotalRequestsSize;
3425
+ delete fileState[id].initialRequestOverhead;
3426
+ }
3427
+
3428
+ function handleResetResponseOnResumeAttempt(id) {
3429
+ fileState[id].attemptingResume = false;
3430
+ log("Server has declared that it cannot handle resume for item ID " + id + " - starting from the first chunk", 'error');
3431
+ handleResetResponse(id);
3432
+ api.upload(id, true);
3433
+ }
3434
+
3435
+ function handleNonResetErrorResponse(id, response, xhr) {
3436
+ var name = api.getName(id);
3437
+
3438
+ if (options.onAutoRetry(id, name, response, xhr)) {
3439
+ return;
3440
+ }
3441
+ else {
3442
+ handleCompletedItem(id, response, xhr);
3443
+ }
3444
+ }
3445
+
3446
+ function onComplete(id, xhr) {
3447
+ var response;
3448
+
3449
+ // the request was aborted/cancelled
3450
+ if (!fileState[id]) {
3451
+ return;
3452
+ }
3453
+
3454
+ log("xhr - server response received for " + id);
3455
+ log("responseText = " + xhr.responseText);
3456
+ response = parseResponse(xhr);
3457
+
3458
+ if (isErrorResponse(xhr, response)) {
3459
+ if (response.reset) {
3460
+ handleResetResponse(id);
3461
+ }
3462
+
3463
+ if (fileState[id].attemptingResume && response.reset) {
3464
+ handleResetResponseOnResumeAttempt(id);
1960
3465
  }
3466
+ else {
3467
+ handleNonResetErrorResponse(id, response, xhr);
3468
+ }
3469
+ }
3470
+ else if (chunkFiles) {
3471
+ handleSuccessfullyCompletedChunk(id, response, xhr);
3472
+ }
3473
+ else {
3474
+ handleCompletedItem(id, response, xhr);
1961
3475
  }
3476
+ }
1962
3477
 
1963
- this._options.onComplete(id, name, response, xhr);
3478
+ function getChunkDataForCallback(chunkData) {
3479
+ return {
3480
+ partIndex: chunkData.part,
3481
+ startByte: chunkData.start + 1,
3482
+ endByte: chunkData.end,
3483
+ totalParts: chunkData.count
3484
+ };
3485
+ }
1964
3486
 
1965
- this._xhrs[id] = null;
1966
- this._dequeue(id);
1967
- },
1968
- _cancel: function(id){
1969
- this._options.onCancel(id, this.getName(id));
3487
+ function getReadyStateChangeHandler(id, xhr) {
3488
+ return function() {
3489
+ if (xhr.readyState === 4) {
3490
+ onComplete(id, xhr);
3491
+ }
3492
+ };
3493
+ }
1970
3494
 
1971
- this._files[id] = null;
3495
+ function persistChunkData(id, chunkData) {
3496
+ var fileUuid = api.getUuid(id),
3497
+ lastByteSent = fileState[id].loaded,
3498
+ initialRequestOverhead = fileState[id].initialRequestOverhead,
3499
+ estTotalRequestsSize = fileState[id].estTotalRequestsSize,
3500
+ cookieName = getChunkDataCookieName(id),
3501
+ cookieValue = fileUuid +
3502
+ cookieItemDelimiter + chunkData.part +
3503
+ cookieItemDelimiter + lastByteSent +
3504
+ cookieItemDelimiter + initialRequestOverhead +
3505
+ cookieItemDelimiter + estTotalRequestsSize,
3506
+ cookieExpDays = options.resume.cookiesExpireIn;
3507
+
3508
+ qq.setCookie(cookieName, cookieValue, cookieExpDays);
3509
+ }
1972
3510
 
1973
- if (this._xhrs[id]){
1974
- this._xhrs[id].abort();
1975
- this._xhrs[id] = null;
3511
+ function deletePersistedChunkData(id) {
3512
+ if (fileState[id].file) {
3513
+ var cookieName = getChunkDataCookieName(id);
3514
+ qq.deleteCookie(cookieName);
1976
3515
  }
1977
3516
  }
1978
- });
3517
+
3518
+ function getPersistedChunkData(id) {
3519
+ var chunkCookieValue = qq.getCookie(getChunkDataCookieName(id)),
3520
+ filename = api.getName(id),
3521
+ sections, uuid, partIndex, lastByteSent, initialRequestOverhead, estTotalRequestsSize;
3522
+
3523
+ if (chunkCookieValue) {
3524
+ sections = chunkCookieValue.split(cookieItemDelimiter);
3525
+
3526
+ if (sections.length === 5) {
3527
+ uuid = sections[0];
3528
+ partIndex = parseInt(sections[1], 10);
3529
+ lastByteSent = parseInt(sections[2], 10);
3530
+ initialRequestOverhead = parseInt(sections[3], 10);
3531
+ estTotalRequestsSize = parseInt(sections[4], 10);
3532
+
3533
+ return {
3534
+ uuid: uuid,
3535
+ part: partIndex,
3536
+ lastByteSent: lastByteSent,
3537
+ initialRequestOverhead: initialRequestOverhead,
3538
+ estTotalRequestsSize: estTotalRequestsSize
3539
+ };
3540
+ }
3541
+ else {
3542
+ log('Ignoring previously stored resume/chunk cookie for ' + filename + " - old cookie format", "warn");
3543
+ }
3544
+ }
3545
+ }
3546
+
3547
+ function getChunkDataCookieName(id) {
3548
+ var filename = api.getName(id),
3549
+ fileSize = api.getSize(id),
3550
+ maxChunkSize = options.chunking.partSize,
3551
+ cookieName;
3552
+
3553
+ cookieName = "qqfilechunk" + cookieItemDelimiter + encodeURIComponent(filename) + cookieItemDelimiter + fileSize + cookieItemDelimiter + maxChunkSize;
3554
+
3555
+ if (resumeId !== undefined) {
3556
+ cookieName += cookieItemDelimiter + resumeId;
3557
+ }
3558
+
3559
+ return cookieName;
3560
+ }
3561
+
3562
+ function getResumeId() {
3563
+ if (options.resume.id !== null &&
3564
+ options.resume.id !== undefined &&
3565
+ !qq.isFunction(options.resume.id) &&
3566
+ !qq.isObject(options.resume.id)) {
3567
+
3568
+ return options.resume.id;
3569
+ }
3570
+ }
3571
+
3572
+ function handleFileChunkingUpload(id, retry) {
3573
+ var name = api.getName(id),
3574
+ firstChunkIndex = 0,
3575
+ persistedChunkInfoForResume, firstChunkDataForResume, currentChunkIndex;
3576
+
3577
+ if (!fileState[id].remainingChunkIdxs || fileState[id].remainingChunkIdxs.length === 0) {
3578
+ fileState[id].remainingChunkIdxs = [];
3579
+
3580
+ if (resumeEnabled && !retry && fileState[id].file) {
3581
+ persistedChunkInfoForResume = getPersistedChunkData(id);
3582
+ if (persistedChunkInfoForResume) {
3583
+ firstChunkDataForResume = getChunkData(id, persistedChunkInfoForResume.part);
3584
+ if (options.onResume(id, name, getChunkDataForCallback(firstChunkDataForResume)) !== false) {
3585
+ firstChunkIndex = persistedChunkInfoForResume.part;
3586
+ fileState[id].uuid = persistedChunkInfoForResume.uuid;
3587
+ fileState[id].loaded = persistedChunkInfoForResume.lastByteSent;
3588
+ fileState[id].estTotalRequestsSize = persistedChunkInfoForResume.estTotalRequestsSize;
3589
+ fileState[id].initialRequestOverhead = persistedChunkInfoForResume.initialRequestOverhead;
3590
+ fileState[id].attemptingResume = true;
3591
+ log('Resuming ' + name + " at partition index " + firstChunkIndex);
3592
+ }
3593
+ }
3594
+ }
3595
+
3596
+ for (currentChunkIndex = getTotalChunks(id)-1; currentChunkIndex >= firstChunkIndex; currentChunkIndex-=1) {
3597
+ fileState[id].remainingChunkIdxs.unshift(currentChunkIndex);
3598
+ }
3599
+ }
3600
+
3601
+ uploadNextChunk(id);
3602
+ }
3603
+
3604
+ function handleStandardFileUpload(id) {
3605
+ var fileOrBlob = fileState[id].file || fileState[id].blobData.blob,
3606
+ name = api.getName(id),
3607
+ xhr, params, toSend;
3608
+
3609
+ fileState[id].loaded = 0;
3610
+
3611
+ xhr = createXhr(id);
3612
+
3613
+ xhr.upload.onprogress = function(e){
3614
+ if (e.lengthComputable){
3615
+ fileState[id].loaded = e.loaded;
3616
+ options.onProgress(id, name, e.loaded, e.total);
3617
+ }
3618
+ };
3619
+
3620
+ xhr.onreadystatechange = getReadyStateChangeHandler(id, xhr);
3621
+
3622
+ params = options.paramsStore.getParams(id);
3623
+ toSend = setParamsAndGetEntityToSend(params, xhr, fileOrBlob, id);
3624
+ setHeaders(id, xhr);
3625
+
3626
+ log('Sending upload request for ' + id);
3627
+ xhr.send(toSend);
3628
+ }
3629
+
3630
+
3631
+ api = {
3632
+ /**
3633
+ * Adds File or Blob to the queue
3634
+ * Returns id to use with upload, cancel
3635
+ **/
3636
+ add: function(fileOrBlobData){
3637
+ var id;
3638
+
3639
+ if (fileOrBlobData instanceof File) {
3640
+ id = fileState.push({file: fileOrBlobData}) - 1;
3641
+ }
3642
+ else if (fileOrBlobData.blob instanceof Blob) {
3643
+ id = fileState.push({blobData: fileOrBlobData}) - 1;
3644
+ }
3645
+ else {
3646
+ throw new Error('Passed obj in not a File or BlobData (in qq.UploadHandlerXhr)');
3647
+ }
3648
+
3649
+ fileState[id].uuid = qq.getUniqueId();
3650
+ return id;
3651
+ },
3652
+ getName: function(id){
3653
+ var file = fileState[id].file,
3654
+ blobData = fileState[id].blobData;
3655
+
3656
+ if (file) {
3657
+ // fix missing name in Safari 4
3658
+ //NOTE: fixed missing name firefox 11.0a2 file.fileName is actually undefined
3659
+ return (file.fileName !== null && file.fileName !== undefined) ? file.fileName : file.name;
3660
+ }
3661
+ else {
3662
+ return blobData.name;
3663
+ }
3664
+ },
3665
+ getSize: function(id){
3666
+ /*jshint eqnull: true*/
3667
+ var fileOrBlob = fileState[id].file || fileState[id].blobData.blob;
3668
+
3669
+ if (qq.isFileOrInput(fileOrBlob)) {
3670
+ return fileOrBlob.fileSize != null ? fileOrBlob.fileSize : fileOrBlob.size;
3671
+ }
3672
+ else {
3673
+ return fileOrBlob.size;
3674
+ }
3675
+ },
3676
+ getFile: function(id) {
3677
+ if (fileState[id]) {
3678
+ return fileState[id].file || fileState[id].blobData.blob;
3679
+ }
3680
+ },
3681
+ /**
3682
+ * Returns uploaded bytes for file identified by id
3683
+ */
3684
+ getLoaded: function(id){
3685
+ return fileState[id].loaded || 0;
3686
+ },
3687
+ isValid: function(id) {
3688
+ return fileState[id] !== undefined;
3689
+ },
3690
+ reset: function() {
3691
+ fileState = [];
3692
+ },
3693
+ getUuid: function(id) {
3694
+ return fileState[id].uuid;
3695
+ },
3696
+ /**
3697
+ * Sends the file identified by id to the server
3698
+ */
3699
+ upload: function(id, retry){
3700
+ var name = this.getName(id);
3701
+
3702
+ options.onUpload(id, name);
3703
+
3704
+ if (chunkFiles) {
3705
+ handleFileChunkingUpload(id, retry);
3706
+ }
3707
+ else {
3708
+ handleStandardFileUpload(id);
3709
+ }
3710
+ },
3711
+ cancel: function(id){
3712
+ var xhr = fileState[id].xhr;
3713
+
3714
+ options.onCancel(id, this.getName(id));
3715
+
3716
+ if (xhr) {
3717
+ xhr.onreadystatechange = null;
3718
+ xhr.abort();
3719
+ }
3720
+
3721
+ if (resumeEnabled) {
3722
+ deletePersistedChunkData(id);
3723
+ }
3724
+
3725
+ delete fileState[id];
3726
+ },
3727
+ getResumableFilesData: function() {
3728
+ var matchingCookieNames = [],
3729
+ resumableFilesData = [];
3730
+
3731
+ if (chunkFiles && resumeEnabled) {
3732
+ if (resumeId === undefined) {
3733
+ matchingCookieNames = qq.getCookieNames(new RegExp("^qqfilechunk\\" + cookieItemDelimiter + ".+\\" +
3734
+ cookieItemDelimiter + "\\d+\\" + cookieItemDelimiter + options.chunking.partSize + "="));
3735
+ }
3736
+ else {
3737
+ matchingCookieNames = qq.getCookieNames(new RegExp("^qqfilechunk\\" + cookieItemDelimiter + ".+\\" +
3738
+ cookieItemDelimiter + "\\d+\\" + cookieItemDelimiter + options.chunking.partSize + "\\" +
3739
+ cookieItemDelimiter + resumeId + "="));
3740
+ }
3741
+
3742
+ qq.each(matchingCookieNames, function(idx, cookieName) {
3743
+ var cookiesNameParts = cookieName.split(cookieItemDelimiter);
3744
+ var cookieValueParts = qq.getCookie(cookieName).split(cookieItemDelimiter);
3745
+
3746
+ resumableFilesData.push({
3747
+ name: decodeURIComponent(cookiesNameParts[1]),
3748
+ size: cookiesNameParts[2],
3749
+ uuid: cookieValueParts[0],
3750
+ partIdx: cookieValueParts[1]
3751
+ });
3752
+ });
3753
+
3754
+ return resumableFilesData;
3755
+ }
3756
+ return [];
3757
+ }
3758
+ };
3759
+
3760
+ return api;
3761
+ };