fineuploader-rails 3.0 → 3.3

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