fileuploader-rails 3.1.1 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -1,4 +1,6 @@
1
- # Fine Uploader 3.0.0 for Rails
1
+ # Fine Uploader 3.4.1 for Rails
2
+
3
+ ### Fineuploader plugin is free for open source projects only under the GPLv3 license.
2
4
 
3
5
  [Fineuploader](http://fineuploader.com/) is a Javascript plugin written by [Andrew Valums](http://github.com/valums/), and actively developed by [Ray Nicholus](http://lnkd.in/Nkhx2C). This plugin uses an XMLHttpRequest (AJAX) for uploading multiple files with a progress-bar in FF3.6+, Safari4+, Chrome and falls back to hidden-iframe-based upload in other browsers (namely IE), providing good user experience everywhere. It does not use Flash, jQuery, or any external libraries.
4
6
 
@@ -6,11 +8,9 @@ This gem integrates this fantastic plugin with Rails 3.1+ Asset Pipeline.
6
8
 
7
9
  [Plugin documentation](https://github.com/valums/file-uploader/blob/master/readme.md)
8
10
 
9
- [Upgrading from 2.1.2](https://github.com/valums/file-uploader/blob/master/readme.md#upgrading-from-212)
10
-
11
11
  ## Installing Gem
12
12
 
13
- gem 'fileuploader-rails', '~> 3.0.0'
13
+ gem 'fileuploader-rails', '~> 3.4.1'
14
14
 
15
15
  ## Using the javascripts
16
16
 
@@ -1,5 +1,5 @@
1
1
  module Fileuploader
2
2
  module Rails
3
- VERSION = "3.1.1"
3
+ VERSION = "3.4.1"
4
4
  end
5
5
  end
@@ -1,14 +1,13 @@
1
1
  /**
2
- * http://github.com/Valums-File-Uploader/file-uploader
2
+ * http://github.com/Widen/fine-uploader
3
3
  *
4
4
  * Multiple file upload component with progress-bar, drag-and-drop, support for all modern browsers.
5
5
  *
6
- * Original version: 1.0 © 2010 Andrew Valums ( andrew(at)valums.com )
7
- * Current Maintainer (2.0+): © 2012, Ray Nicholus ( fineuploader(at)garstasio.com )
6
+ * Copyright © 2013, Widen Enterprises info@fineupoader.com
8
7
  *
9
- * Licensed under MIT license, GNU GPL 2 or later, GNU LGPL 2 or later, see license.txt.
8
+ * Licensed under GNU GPL v3, see license.txt.
10
9
  */
11
- /*globals window, navigator, document, FormData, File, HTMLInputElement, XMLHttpRequest*/
10
+ /*globals window, navigator, document, FormData, File, HTMLInputElement, XMLHttpRequest, Blob*/
12
11
  var qq = function(element) {
13
12
  "use strict";
14
13
 
@@ -172,9 +171,22 @@ qq.isFunction = function(variable) {
172
171
  return typeof(variable) === "function";
173
172
  };
174
173
 
174
+ qq.isString = function(maybeString) {
175
+ "use strict";
176
+ return Object.prototype.toString.call(maybeString) === '[object String]';
177
+ };
178
+
179
+ qq.trimStr = function(string) {
180
+ if (String.prototype.trim) {
181
+ return string.trim();
182
+ }
183
+
184
+ return string.replace(/^\s+|\s+$/g,'');
185
+ };
186
+
175
187
  qq.isFileOrInput = function(maybeFileOrInput) {
176
188
  "use strict";
177
- if (window.File && maybeFileOrInput instanceof File) {
189
+ if (qq.isBlob(maybeFileOrInput) && window.File && maybeFileOrInput instanceof File) {
178
190
  return true;
179
191
  }
180
192
  else if (window.HTMLInputElement) {
@@ -195,6 +207,11 @@ qq.isFileOrInput = function(maybeFileOrInput) {
195
207
  return false;
196
208
  };
197
209
 
210
+ qq.isBlob = function(maybeBlob) {
211
+ "use strict";
212
+ return window.Blob && maybeBlob instanceof Blob;
213
+ };
214
+
198
215
  qq.isXhrUploadSupported = function() {
199
216
  "use strict";
200
217
  var input = document.createElement('input');
@@ -212,6 +229,13 @@ qq.isFolderDropSupported = function(dataTransfer) {
212
229
  return (dataTransfer.items && dataTransfer.items[0].webkitGetAsEntry);
213
230
  };
214
231
 
232
+ qq.isFileChunkingSupported = function() {
233
+ "use strict";
234
+ return !qq.android() && //android's impl of Blob.slice is broken
235
+ qq.isXhrUploadSupported() &&
236
+ (File.prototype.slice || File.prototype.webkitSlice || File.prototype.mozSlice);
237
+ };
238
+
215
239
  qq.extend = function (first, second, extendNested) {
216
240
  "use strict";
217
241
  qq.each(second, function(prop, val) {
@@ -245,7 +269,7 @@ qq.indexOf = function(arr, elt, from){
245
269
  from += len;
246
270
  }
247
271
 
248
- for (null; from < len; from+=1){
272
+ for (; from < len; from+=1){
249
273
  if (arr.hasOwnProperty(from) && arr[from] === elt){
250
274
  return from;
251
275
  }
@@ -253,15 +277,16 @@ qq.indexOf = function(arr, elt, from){
253
277
  return -1;
254
278
  };
255
279
 
256
- qq.getUniqueId = (function(){
280
+ //this is a version 4 UUID
281
+ qq.getUniqueId = function(){
257
282
  "use strict";
258
283
 
259
- var id = -1;
260
- return function(){
261
- id += 1;
262
- return id;
263
- };
264
- }());
284
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
285
+ /*jslint eqeq: true, bitwise: true*/
286
+ var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
287
+ return v.toString(16);
288
+ });
289
+ };
265
290
 
266
291
  //
267
292
  // Browsers and platforms detection
@@ -290,6 +315,10 @@ qq.windows = function(){
290
315
  "use strict";
291
316
  return navigator.platform === "Win32";
292
317
  };
318
+ qq.android = function(){
319
+ "use strict";
320
+ return navigator.userAgent.toLowerCase().indexOf('android') !== -1;
321
+ };
293
322
 
294
323
  //
295
324
  // Events
@@ -352,6 +381,7 @@ qq.each = function(obj, callback) {
352
381
  */
353
382
  qq.obj2url = function(obj, temp, prefixDone){
354
383
  "use strict";
384
+ /*jshint laxbreak: true*/
355
385
  var i, len,
356
386
  uristrings = [],
357
387
  prefix = '&',
@@ -414,10 +444,10 @@ qq.obj2FormData = function(obj, formData, arrayKeyName) {
414
444
  qq.obj2FormData(val, formData, key);
415
445
  }
416
446
  else if (qq.isFunction(val)) {
417
- formData.append(encodeURIComponent(key), encodeURIComponent(val()));
447
+ formData.append(key, val());
418
448
  }
419
449
  else {
420
- formData.append(encodeURIComponent(key), encodeURIComponent(val));
450
+ formData.append(key, val);
421
451
  }
422
452
  });
423
453
 
@@ -444,6 +474,80 @@ qq.obj2Inputs = function(obj, form) {
444
474
  return form;
445
475
  };
446
476
 
477
+ qq.setCookie = function(name, value, days) {
478
+ var date = new Date(),
479
+ expires = "";
480
+
481
+ if (days) {
482
+ date.setTime(date.getTime()+(days*24*60*60*1000));
483
+ expires = "; expires="+date.toGMTString();
484
+ }
485
+
486
+ document.cookie = name+"="+value+expires+"; path=/";
487
+ };
488
+
489
+ qq.getCookie = function(name) {
490
+ var nameEQ = name + "=",
491
+ ca = document.cookie.split(';'),
492
+ c;
493
+
494
+ for(var i=0;i < ca.length;i++) {
495
+ c = ca[i];
496
+ while (c.charAt(0)==' ') {
497
+ c = c.substring(1,c.length);
498
+ }
499
+ if (c.indexOf(nameEQ) === 0) {
500
+ return c.substring(nameEQ.length,c.length);
501
+ }
502
+ }
503
+ };
504
+
505
+ qq.getCookieNames = function(regexp) {
506
+ var cookies = document.cookie.split(';'),
507
+ cookieNames = [];
508
+
509
+ qq.each(cookies, function(idx, cookie) {
510
+ cookie = qq.trimStr(cookie);
511
+
512
+ var equalsIdx = cookie.indexOf("=");
513
+
514
+ if (cookie.match(regexp)) {
515
+ cookieNames.push(cookie.substr(0, equalsIdx));
516
+ }
517
+ });
518
+
519
+ return cookieNames;
520
+ };
521
+
522
+ qq.deleteCookie = function(name) {
523
+ qq.setCookie(name, "", -1);
524
+ };
525
+
526
+ qq.areCookiesEnabled = function() {
527
+ var randNum = Math.random() * 100000,
528
+ name = "qqCookieTest:" + randNum;
529
+ qq.setCookie(name, 1);
530
+
531
+ if (qq.getCookie(name)) {
532
+ qq.deleteCookie(name);
533
+ return true;
534
+ }
535
+ return false;
536
+ };
537
+
538
+ /**
539
+ * Not recommended for use outside of Fine Uploader since this falls back to an unchecked eval if JSON.parse is not
540
+ * implemented. For a more secure JSON.parse polyfill, use Douglas Crockford's json2.js.
541
+ */
542
+ qq.parseJson = function(json) {
543
+ /*jshint evil: true*/
544
+ if (window.JSON && qq.isFunction(JSON.parse)) {
545
+ return JSON.parse(json);
546
+ } else {
547
+ return eval("(" + json + ")");
548
+ }
549
+ };
550
+
447
551
  /**
448
552
  * A generic module which supports object disposing in dispose() method.
449
553
  * */
@@ -477,61 +581,82 @@ qq.DisposeSupport = function() {
477
581
  }
478
582
  };
479
583
  };
480
- qq.UploadButton = function(o){
481
- this._options = {
482
- element: null,
483
- // if set to true adds multiple attribute to file input
484
- multiple: false,
485
- acceptFiles: null,
486
- // name attribute of file input
487
- name: 'file',
488
- onChange: function(input){},
489
- hoverClass: 'qq-upload-button-hover',
490
- focusClass: 'qq-upload-button-focus'
491
- };
584
+ /*globals qq*/
585
+ qq.Promise = function() {
586
+ "use strict";
492
587
 
493
- qq.extend(this._options, o);
494
- this._disposeSupport = new qq.DisposeSupport();
588
+ var successValue, failureValue,
589
+ successCallback, failureCallback,
590
+ state = 0;
495
591
 
496
- this._element = this._options.element;
592
+ return {
593
+ then: function(onSuccess, onFailure) {
594
+ if (state === 0) {
595
+ successCallback = onSuccess;
596
+ failureCallback = onFailure;
597
+ }
598
+ else if (state === -1 && onFailure) {
599
+ onFailure(failureValue);
600
+ }
601
+ else if (onSuccess) {
602
+ onSuccess(successValue);
603
+ }
604
+ },
497
605
 
498
- // make button suitable container for input
499
- qq(this._element).css({
500
- position: 'relative',
501
- overflow: 'hidden',
502
- // Make sure browse button is in the right side
503
- // in Internet Explorer
504
- direction: 'ltr'
505
- });
606
+ success: function(val) {
607
+ state = 1;
608
+ successValue = val;
506
609
 
507
- this._input = this._createInput();
508
- };
610
+ if (successCallback) {
611
+ successCallback(val);
612
+ }
509
613
 
510
- qq.UploadButton.prototype = {
511
- /* returns file input element */
512
- getInput: function(){
513
- return this._input;
514
- },
515
- /* cleans/recreates the file input */
516
- reset: function(){
517
- if (this._input.parentNode){
518
- qq(this._input).remove();
614
+ return this;
615
+ },
616
+
617
+ failure: function(val) {
618
+ state = -1;
619
+ failureValue = val;
620
+
621
+ if (failureCallback) {
622
+ failureCallback(val);
623
+ }
624
+
625
+ return this;
519
626
  }
627
+ };
628
+ };
629
+ /*globals qq*/
630
+ qq.UploadButton = function(o) {
631
+ "use strict";
520
632
 
521
- qq(this._element).removeClass(this._options.focusClass);
522
- this._input = this._createInput();
523
- },
524
- _createInput: function(){
633
+ var input,
634
+ disposeSupport = new qq.DisposeSupport(),
635
+ options = {
636
+ element: null,
637
+ // if set to true adds multiple attribute to file input
638
+ multiple: false,
639
+ acceptFiles: null,
640
+ // name attribute of file input
641
+ name: 'file',
642
+ onChange: function(input) {},
643
+ hoverClass: 'qq-upload-button-hover',
644
+ focusClass: 'qq-upload-button-focus'
645
+ };
646
+
647
+ function createInput() {
525
648
  var input = document.createElement("input");
526
649
 
527
- if (this._options.multiple){
650
+ if (options.multiple){
528
651
  input.setAttribute("multiple", "multiple");
529
652
  }
530
653
 
531
- if (this._options.acceptFiles) input.setAttribute("accept", this._options.acceptFiles);
654
+ if (options.acceptFiles) {
655
+ input.setAttribute("accept", options.acceptFiles);
656
+ }
532
657
 
533
658
  input.setAttribute("type", "file");
534
- input.setAttribute("name", this._options.name);
659
+ input.setAttribute("name", options.name);
535
660
 
536
661
  qq(input).css({
537
662
  position: 'absolute',
@@ -549,24 +674,23 @@ qq.UploadButton.prototype = {
549
674
  opacity: 0
550
675
  });
551
676
 
552
- this._element.appendChild(input);
677
+ options.element.appendChild(input);
553
678
 
554
- var self = this;
555
- this._disposeSupport.attach(input, 'change', function(){
556
- self._options.onChange(input);
679
+ disposeSupport.attach(input, 'change', function(){
680
+ options.onChange(input);
557
681
  });
558
682
 
559
- this._disposeSupport.attach(input, 'mouseover', function(){
560
- qq(self._element).addClass(self._options.hoverClass);
683
+ disposeSupport.attach(input, 'mouseover', function(){
684
+ qq(options.element).addClass(options.hoverClass);
561
685
  });
562
- this._disposeSupport.attach(input, 'mouseout', function(){
563
- qq(self._element).removeClass(self._options.hoverClass);
686
+ disposeSupport.attach(input, 'mouseout', function(){
687
+ qq(options.element).removeClass(options.hoverClass);
564
688
  });
565
- this._disposeSupport.attach(input, 'focus', function(){
566
- qq(self._element).addClass(self._options.focusClass);
689
+ disposeSupport.attach(input, 'focus', function(){
690
+ qq(options.element).addClass(options.focusClass);
567
691
  });
568
- this._disposeSupport.attach(input, 'blur', function(){
569
- qq(self._element).removeClass(self._options.focusClass);
692
+ disposeSupport.attach(input, 'blur', function(){
693
+ qq(options.element).removeClass(options.focusClass);
570
694
  });
571
695
 
572
696
  // IE and Opera, unfortunately have 2 tab stops on file input
@@ -578,6 +702,84 @@ qq.UploadButton.prototype = {
578
702
 
579
703
  return input;
580
704
  }
705
+
706
+
707
+ qq.extend(options, o);
708
+
709
+ // make button suitable container for input
710
+ qq(options.element).css({
711
+ position: 'relative',
712
+ overflow: 'hidden',
713
+ // Make sure browse button is in the right side
714
+ // in Internet Explorer
715
+ direction: 'ltr'
716
+ });
717
+
718
+ input = createInput();
719
+
720
+ return {
721
+ getInput: function(){
722
+ return input;
723
+ },
724
+
725
+ reset: function(){
726
+ if (input.parentNode){
727
+ qq(input).remove();
728
+ }
729
+
730
+ qq(options.element).removeClass(options.focusClass);
731
+ input = createInput();
732
+ }
733
+ };
734
+ };
735
+ /*globals qq*/
736
+ qq.PasteSupport = function(o) {
737
+ "use strict";
738
+
739
+ var options, detachPasteHandler;
740
+
741
+ options = {
742
+ targetElement: null,
743
+ callbacks: {
744
+ log: function(message, level) {},
745
+ pasteReceived: function(blob) {}
746
+ }
747
+ };
748
+
749
+ function isImage(item) {
750
+ return item.type &&
751
+ item.type.indexOf("image/") === 0;
752
+ }
753
+
754
+ function registerPasteHandler() {
755
+ qq(options.targetElement).attach("paste", function(event) {
756
+ var clipboardData = event.clipboardData;
757
+
758
+ if (clipboardData) {
759
+ qq.each(clipboardData.items, function(idx, item) {
760
+ if (isImage(item)) {
761
+ var blob = item.getAsFile();
762
+ options.callbacks.pasteReceived(blob);
763
+ }
764
+ });
765
+ }
766
+ });
767
+ }
768
+
769
+ function unregisterPasteHandler() {
770
+ if (detachPasteHandler) {
771
+ detachPasteHandler();
772
+ }
773
+ }
774
+
775
+ qq.extend(options, o);
776
+ registerPasteHandler();
777
+
778
+ return {
779
+ reset: function() {
780
+ unregisterPasteHandler();
781
+ }
782
+ };
581
783
  };
582
784
  qq.FineUploaderBasic = function(o){
583
785
  var that = this;
@@ -591,27 +793,40 @@ qq.FineUploaderBasic = function(o){
591
793
  request: {
592
794
  endpoint: '/server/upload',
593
795
  params: {},
594
- paramsInBody: false,
796
+ paramsInBody: true,
595
797
  customHeaders: {},
596
- forceMultipart: false,
597
- inputName: 'qqfile'
798
+ forceMultipart: true,
799
+ inputName: 'qqfile',
800
+ uuidName: 'qquuid',
801
+ totalFileSizeName: 'qqtotalfilesize'
598
802
  },
599
803
  validation: {
600
804
  allowedExtensions: [],
601
805
  sizeLimit: 0,
602
806
  minSizeLimit: 0,
807
+ itemLimit: 0,
603
808
  stopOnFirstInvalidFile: true
604
809
  },
605
810
  callbacks: {
606
- onSubmit: function(id, fileName){}, // return false to cancel submit
607
- onComplete: function(id, fileName, responseJSON){},
608
- onCancel: function(id, fileName){},
609
- onUpload: function(id, fileName, xhr){},
610
- onProgress: function(id, fileName, loaded, total){},
611
- onError: function(id, fileName, reason) {},
612
- onAutoRetry: function(id, fileName, attemptNumber) {},
613
- onManualRetry: function(id, fileName) {},
614
- onValidate: function(fileData) {} // return false to prevent upload
811
+ onSubmit: function(id, name){},
812
+ onSubmitted: function(id, name){},
813
+ onComplete: function(id, name, responseJSON){},
814
+ onCancel: function(id, name){},
815
+ onUpload: function(id, name){},
816
+ onUploadChunk: function(id, name, chunkData){},
817
+ onResume: function(id, fileName, chunkData){},
818
+ onProgress: function(id, name, loaded, total){},
819
+ onError: function(id, name, reason, maybeXhr) {},
820
+ onAutoRetry: function(id, name, attemptNumber) {},
821
+ onManualRetry: function(id, name) {},
822
+ onValidateBatch: function(fileOrBlobData) {},
823
+ onValidate: function(fileOrBlobData) {},
824
+ onSubmitDelete: function(id) {},
825
+ onDelete: function(id){},
826
+ onDeleteComplete: function(id, xhr, isError){},
827
+ onPasteReceived: function(blob) {
828
+ return new qq.Promise().success();
829
+ }
615
830
  },
616
831
  messages: {
617
832
  typeError: "{file} has an invalid extension. Valid extension(s): {extensions}.",
@@ -619,6 +834,8 @@ qq.FineUploaderBasic = function(o){
619
834
  minSizeError: "{file} is too small, minimum file size is {minSizeLimit}.",
620
835
  emptyError: "{file} is empty, please select files again without it.",
621
836
  noFilesError: "No files to upload.",
837
+ tooManyItemsError: "Too many items ({netItems}) would be uploaded. Item limit is {itemLimit}.",
838
+ retryFailTooManyItems: "Retry failed - you have reached your file limit.",
622
839
  onLeave: "The files are being uploaded, if you leave now the upload will be cancelled."
623
840
  },
624
841
  retry: {
@@ -630,6 +847,55 @@ qq.FineUploaderBasic = function(o){
630
847
  classes: {
631
848
  buttonHover: 'qq-upload-button-hover',
632
849
  buttonFocus: 'qq-upload-button-focus'
850
+ },
851
+ chunking: {
852
+ enabled: false,
853
+ partSize: 2000000,
854
+ paramNames: {
855
+ partIndex: 'qqpartindex',
856
+ partByteOffset: 'qqpartbyteoffset',
857
+ chunkSize: 'qqchunksize',
858
+ totalFileSize: 'qqtotalfilesize',
859
+ totalParts: 'qqtotalparts',
860
+ filename: 'qqfilename'
861
+ }
862
+ },
863
+ resume: {
864
+ enabled: false,
865
+ id: null,
866
+ cookiesExpireIn: 7, //days
867
+ paramNames: {
868
+ resuming: "qqresume"
869
+ }
870
+ },
871
+ formatFileName: function(fileOrBlobName) {
872
+ if (fileOrBlobName.length > 33) {
873
+ fileOrBlobName = fileOrBlobName.slice(0, 19) + '...' + fileOrBlobName.slice(-14);
874
+ }
875
+ return fileOrBlobName;
876
+ },
877
+ text: {
878
+ sizeSymbols: ['kB', 'MB', 'GB', 'TB', 'PB', 'EB']
879
+ },
880
+ deleteFile : {
881
+ enabled: false,
882
+ endpoint: '/server/upload',
883
+ customHeaders: {},
884
+ params: {}
885
+ },
886
+ cors: {
887
+ expected: false,
888
+ sendCredentials: false
889
+ },
890
+ blobs: {
891
+ defaultName: 'misc_data',
892
+ paramNames: {
893
+ name: 'qqblobname'
894
+ }
895
+ },
896
+ paste: {
897
+ targetElement: null,
898
+ defaultName: 'pasted_image'
633
899
  }
634
900
  };
635
901
 
@@ -637,23 +903,30 @@ qq.FineUploaderBasic = function(o){
637
903
  this._wrapCallbacks();
638
904
  this._disposeSupport = new qq.DisposeSupport();
639
905
 
640
- // number of files being uploaded
641
- this._filesInProgress = 0;
642
-
643
- this._storedFileIds = [];
644
-
906
+ this._filesInProgress = [];
907
+ this._storedIds = [];
645
908
  this._autoRetries = [];
646
909
  this._retryTimeouts = [];
647
910
  this._preventRetries = [];
911
+ this._netFilesUploadedOrQueued = 0;
912
+
913
+ this._paramsStore = this._createParamsStore("request");
914
+ this._deleteFileParamsStore = this._createParamsStore("deleteFile");
648
915
 
649
- this._paramsStore = this._createParamsStore();
916
+ this._endpointStore = this._createEndpointStore("request");
917
+ this._deleteFileEndpointStore = this._createEndpointStore("deleteFile");
650
918
 
651
919
  this._handler = this._createUploadHandler();
920
+ this._deleteHandler = this._createDeleteHandler();
652
921
 
653
922
  if (this._options.button){
654
923
  this._button = this._createUploadButton(this._options.button);
655
924
  }
656
925
 
926
+ if (this._options.paste.targetElement) {
927
+ this._pasteHandler = this._createPasteHandler();
928
+ }
929
+
657
930
  this._preventLeaveInProgress();
658
931
  };
659
932
 
@@ -667,29 +940,52 @@ qq.FineUploaderBasic.prototype = {
667
940
 
668
941
  }
669
942
  },
670
- setParams: function(params, fileId){
671
- if (fileId === undefined) {
943
+ setParams: function(params, id) {
944
+ /*jshint eqeqeq: true, eqnull: true*/
945
+ if (id == null) {
672
946
  this._options.request.params = params;
673
947
  }
674
948
  else {
675
- this._paramsStore.setParams(params, fileId);
949
+ this._paramsStore.setParams(params, id);
950
+ }
951
+ },
952
+ setDeleteFileParams: function(params, id) {
953
+ /*jshint eqeqeq: true, eqnull: true*/
954
+ if (id == null) {
955
+ this._options.deleteFile.params = params;
956
+ }
957
+ else {
958
+ this._deleteFileParamsStore.setParams(params, id);
959
+ }
960
+ },
961
+ setEndpoint: function(endpoint, id) {
962
+ /*jshint eqeqeq: true, eqnull: true*/
963
+ if (id == null) {
964
+ this._options.request.endpoint = endpoint;
965
+ }
966
+ else {
967
+ this._endpointStore.setEndpoint(endpoint, id);
676
968
  }
677
969
  },
678
970
  getInProgress: function(){
679
- return this._filesInProgress;
971
+ return this._filesInProgress.length;
680
972
  },
681
973
  uploadStoredFiles: function(){
682
974
  "use strict";
683
- while(this._storedFileIds.length) {
684
- this._filesInProgress++;
685
- this._handler.upload(this._storedFileIds.shift());
975
+ var idToUpload;
976
+
977
+ while(this._storedIds.length) {
978
+ idToUpload = this._storedIds.shift();
979
+ this._filesInProgress.push(idToUpload);
980
+ this._handler.upload(idToUpload);
686
981
  }
687
982
  },
688
983
  clearStoredFiles: function(){
689
- this._storedFileIds = [];
984
+ this._storedIds = [];
690
985
  },
691
986
  retry: function(id) {
692
987
  if (this._onBeforeManualRetry(id)) {
988
+ this._netFilesUploadedOrQueued++;
693
989
  this._handler.retry(id);
694
990
  return true;
695
991
  }
@@ -697,32 +993,49 @@ qq.FineUploaderBasic.prototype = {
697
993
  return false;
698
994
  }
699
995
  },
700
- cancel: function(fileId) {
701
- this._handler.cancel(fileId);
996
+ cancel: function(id) {
997
+ this._handler.cancel(id);
998
+ },
999
+ cancelAll: function() {
1000
+ var storedIdsCopy = [],
1001
+ self = this;
1002
+
1003
+ qq.extend(storedIdsCopy, this._storedIds);
1004
+ qq.each(storedIdsCopy, function(idx, storedFileId) {
1005
+ self.cancel(storedFileId);
1006
+ });
1007
+
1008
+ this._handler.cancelAll();
702
1009
  },
703
1010
  reset: function() {
704
1011
  this.log("Resetting uploader...");
705
1012
  this._handler.reset();
706
- this._filesInProgress = 0;
707
- this._storedFileIds = [];
1013
+ this._filesInProgress = [];
1014
+ this._storedIds = [];
708
1015
  this._autoRetries = [];
709
1016
  this._retryTimeouts = [];
710
1017
  this._preventRetries = [];
711
1018
  this._button.reset();
712
1019
  this._paramsStore.reset();
1020
+ this._endpointStore.reset();
1021
+ this._netFilesUploadedOrQueued = 0;
1022
+
1023
+ if (this._pasteHandler) {
1024
+ this._pasteHandler.reset();
1025
+ }
713
1026
  },
714
- addFiles: function(filesOrInputs) {
1027
+ addFiles: function(filesBlobDataOrInputs) {
715
1028
  var self = this,
716
1029
  verifiedFilesOrInputs = [],
717
1030
  index, fileOrInput;
718
1031
 
719
- if (filesOrInputs) {
720
- if (!window.FileList || !(filesOrInputs instanceof FileList)) {
721
- filesOrInputs = [].concat(filesOrInputs);
1032
+ if (filesBlobDataOrInputs) {
1033
+ if (!window.FileList || !(filesBlobDataOrInputs instanceof FileList)) {
1034
+ filesBlobDataOrInputs = [].concat(filesBlobDataOrInputs);
722
1035
  }
723
1036
 
724
- for (index = 0; index < filesOrInputs.length; index+=1) {
725
- fileOrInput = filesOrInputs[index];
1037
+ for (index = 0; index < filesBlobDataOrInputs.length; index+=1) {
1038
+ fileOrInput = filesBlobDataOrInputs[index];
726
1039
 
727
1040
  if (qq.isFileOrInput(fileOrInput)) {
728
1041
  verifiedFilesOrInputs.push(fileOrInput);
@@ -733,9 +1046,66 @@ qq.FineUploaderBasic.prototype = {
733
1046
  }
734
1047
 
735
1048
  this.log('Processing ' + verifiedFilesOrInputs.length + ' files or inputs...');
736
- this._uploadFileList(verifiedFilesOrInputs);
1049
+ this._uploadFileOrBlobDataList(verifiedFilesOrInputs);
1050
+ }
1051
+ },
1052
+ addBlobs: function(blobDataOrArray) {
1053
+ if (blobDataOrArray) {
1054
+ var blobDataArray = [].concat(blobDataOrArray),
1055
+ verifiedBlobDataList = [],
1056
+ self = this;
1057
+
1058
+ qq.each(blobDataArray, function(idx, blobData) {
1059
+ if (qq.isBlob(blobData) && !qq.isFileOrInput(blobData)) {
1060
+ verifiedBlobDataList.push({
1061
+ blob: blobData,
1062
+ name: self._options.blobs.defaultName
1063
+ });
1064
+ }
1065
+ else if (qq.isObject(blobData) && blobData.blob && blobData.name) {
1066
+ verifiedBlobDataList.push(blobData);
1067
+ }
1068
+ else {
1069
+ self.log("addBlobs: entry at index " + idx + " is not a Blob or a BlobData object", "error");
1070
+ }
1071
+ });
1072
+
1073
+ this._uploadFileOrBlobDataList(verifiedBlobDataList);
1074
+ }
1075
+ else {
1076
+ this.log("undefined or non-array parameter passed into addBlobs", "error");
1077
+ }
1078
+ },
1079
+ getUuid: function(id) {
1080
+ return this._handler.getUuid(id);
1081
+ },
1082
+ getResumableFilesData: function() {
1083
+ return this._handler.getResumableFilesData();
1084
+ },
1085
+ getSize: function(id) {
1086
+ return this._handler.getSize(id);
1087
+ },
1088
+ getName: function(id) {
1089
+ return this._handler.getName(id);
1090
+ },
1091
+ getFile: function(fileOrBlobId) {
1092
+ return this._handler.getFile(fileOrBlobId);
1093
+ },
1094
+ deleteFile: function(id) {
1095
+ this._onSubmitDelete(id);
1096
+ },
1097
+ setDeleteFileEndpoint: function(endpoint, id) {
1098
+ /*jshint eqeqeq: true, eqnull: true*/
1099
+ if (id == null) {
1100
+ this._options.deleteFile.endpoint = endpoint;
1101
+ }
1102
+ else {
1103
+ this._deleteFileEndpointStore.setEndpoint(endpoint, id);
737
1104
  }
738
1105
  },
1106
+ getPromissoryCallbackNames: function() {
1107
+ return ["onPasteReceived"];
1108
+ },
739
1109
  _createUploadButton: function(element){
740
1110
  var self = this;
741
1111
 
@@ -754,52 +1124,59 @@ qq.FineUploaderBasic.prototype = {
754
1124
  return button;
755
1125
  },
756
1126
  _createUploadHandler: function(){
757
- var self = this,
758
- handlerClass;
759
-
760
- if(qq.isXhrUploadSupported()){
761
- handlerClass = 'UploadHandlerXhr';
762
- } else {
763
- handlerClass = 'UploadHandlerForm';
764
- }
1127
+ var self = this;
765
1128
 
766
- var handler = new qq[handlerClass]({
1129
+ return new qq.UploadHandler({
767
1130
  debug: this._options.debug,
768
- endpoint: this._options.request.endpoint,
769
1131
  forceMultipart: this._options.request.forceMultipart,
770
1132
  maxConnections: this._options.maxConnections,
771
1133
  customHeaders: this._options.request.customHeaders,
772
1134
  inputName: this._options.request.inputName,
1135
+ uuidParamName: this._options.request.uuidName,
1136
+ totalFileSizeParamName: this._options.request.totalFileSizeName,
1137
+ cors: this._options.cors,
773
1138
  demoMode: this._options.demoMode,
774
- log: this.log,
775
1139
  paramsInBody: this._options.request.paramsInBody,
776
1140
  paramsStore: this._paramsStore,
777
- onProgress: function(id, fileName, loaded, total){
778
- self._onProgress(id, fileName, loaded, total);
779
- self._options.callbacks.onProgress(id, fileName, loaded, total);
1141
+ endpointStore: this._endpointStore,
1142
+ chunking: this._options.chunking,
1143
+ resume: this._options.resume,
1144
+ blobs: this._options.blobs,
1145
+ log: function(str, level) {
1146
+ self.log(str, level);
780
1147
  },
781
- onComplete: function(id, fileName, result, xhr){
782
- self._onComplete(id, fileName, result, xhr);
783
- self._options.callbacks.onComplete(id, fileName, result);
1148
+ onProgress: function(id, name, loaded, total){
1149
+ self._onProgress(id, name, loaded, total);
1150
+ self._options.callbacks.onProgress(id, name, loaded, total);
784
1151
  },
785
- onCancel: function(id, fileName){
786
- self._onCancel(id, fileName);
787
- self._options.callbacks.onCancel(id, fileName);
1152
+ onComplete: function(id, name, result, xhr){
1153
+ self._onComplete(id, name, result, xhr);
1154
+ self._options.callbacks.onComplete(id, name, result);
788
1155
  },
789
- onUpload: function(id, fileName, xhr){
790
- self._onUpload(id, fileName, xhr);
791
- self._options.callbacks.onUpload(id, fileName, xhr);
1156
+ onCancel: function(id, name){
1157
+ self._onCancel(id, name);
1158
+ self._options.callbacks.onCancel(id, name);
792
1159
  },
793
- onAutoRetry: function(id, fileName, responseJSON, xhr) {
1160
+ onUpload: function(id, name){
1161
+ self._onUpload(id, name);
1162
+ self._options.callbacks.onUpload(id, name);
1163
+ },
1164
+ onUploadChunk: function(id, name, chunkData){
1165
+ self._options.callbacks.onUploadChunk(id, name, chunkData);
1166
+ },
1167
+ onResume: function(id, name, chunkData) {
1168
+ return self._options.callbacks.onResume(id, name, chunkData);
1169
+ },
1170
+ onAutoRetry: function(id, name, responseJSON, xhr) {
794
1171
  self._preventRetries[id] = responseJSON[self._options.retry.preventRetryResponseProperty];
795
1172
 
796
- if (self._shouldAutoRetry(id, fileName, responseJSON)) {
797
- self._maybeParseAndSendUploadError(id, fileName, responseJSON, xhr);
798
- self._options.callbacks.onAutoRetry(id, fileName, self._autoRetries[id] + 1);
799
- self._onBeforeAutoRetry(id, fileName);
1173
+ if (self._shouldAutoRetry(id, name, responseJSON)) {
1174
+ self._maybeParseAndSendUploadError(id, name, responseJSON, xhr);
1175
+ self._options.callbacks.onAutoRetry(id, name, self._autoRetries[id] + 1);
1176
+ self._onBeforeAutoRetry(id, name);
800
1177
 
801
1178
  self._retryTimeouts[id] = setTimeout(function() {
802
- self._onAutoRetry(id, fileName, responseJSON)
1179
+ self._onAutoRetry(id, name, responseJSON)
803
1180
  }, self._options.retry.autoAttemptDelay * 1000);
804
1181
 
805
1182
  return true;
@@ -809,14 +1186,79 @@ qq.FineUploaderBasic.prototype = {
809
1186
  }
810
1187
  }
811
1188
  });
1189
+ },
1190
+ _createDeleteHandler: function() {
1191
+ var self = this;
1192
+
1193
+ return new qq.DeleteFileAjaxRequestor({
1194
+ maxConnections: this._options.maxConnections,
1195
+ customHeaders: this._options.deleteFile.customHeaders,
1196
+ paramsStore: this._deleteFileParamsStore,
1197
+ endpointStore: this._deleteFileEndpointStore,
1198
+ demoMode: this._options.demoMode,
1199
+ cors: this._options.cors,
1200
+ log: function(str, level) {
1201
+ self.log(str, level);
1202
+ },
1203
+ onDelete: function(id) {
1204
+ self._onDelete(id);
1205
+ self._options.callbacks.onDelete(id);
1206
+ },
1207
+ onDeleteComplete: function(id, xhr, isError) {
1208
+ self._onDeleteComplete(id, xhr, isError);
1209
+ self._options.callbacks.onDeleteComplete(id, xhr, isError);
1210
+ }
1211
+
1212
+ });
1213
+ },
1214
+ _createPasteHandler: function() {
1215
+ var self = this;
1216
+
1217
+ return new qq.PasteSupport({
1218
+ targetElement: this._options.paste.targetElement,
1219
+ callbacks: {
1220
+ log: function(str, level) {
1221
+ self.log(str, level);
1222
+ },
1223
+ pasteReceived: function(blob) {
1224
+ var pasteReceivedCallback = self._options.callbacks.onPasteReceived,
1225
+ promise = pasteReceivedCallback(blob);
1226
+
1227
+ if (promise.then) {
1228
+ promise.then(function(successData) {
1229
+ self._handlePasteSuccess(blob, successData);
1230
+ }, function(failureData) {
1231
+ self.log("Ignoring pasted image per paste received callback. Reason = '" + failureData + "'");
1232
+ });
1233
+ }
1234
+ else {
1235
+ self.log("Promise contract not fulfilled in pasteReceived callback handler! Ignoring pasted item.", "error");
1236
+ }
1237
+ }
1238
+ }
1239
+ });
1240
+ },
1241
+ _handlePasteSuccess: function(blob, extSuppliedName) {
1242
+ var extension = blob.type.split("/")[1],
1243
+ name = extSuppliedName;
1244
+
1245
+ /*jshint eqeqeq: true, eqnull: true*/
1246
+ if (name == null) {
1247
+ name = this._options.paste.defaultName;
1248
+ }
1249
+
1250
+ name += '.' + extension;
812
1251
 
813
- return handler;
1252
+ this.addBlobs({
1253
+ name: name,
1254
+ blob: blob
1255
+ });
814
1256
  },
815
1257
  _preventLeaveInProgress: function(){
816
1258
  var self = this;
817
1259
 
818
1260
  this._disposeSupport.attach(window, 'beforeunload', function(e){
819
- if (!self._filesInProgress){return;}
1261
+ if (!self._filesInProgress.length){return;}
820
1262
 
821
1263
  var e = e || window.event;
822
1264
  // for ie, ff
@@ -825,61 +1267,105 @@ qq.FineUploaderBasic.prototype = {
825
1267
  return self._options.messages.onLeave;
826
1268
  });
827
1269
  },
828
- _onSubmit: function(id, fileName){
1270
+ _onSubmit: function(id, name) {
1271
+ this._netFilesUploadedOrQueued++;
1272
+
829
1273
  if (this._options.autoUpload) {
830
- this._filesInProgress++;
1274
+ this._filesInProgress.push(id);
831
1275
  }
832
1276
  },
833
- _onProgress: function(id, fileName, loaded, total){
1277
+ _onProgress: function(id, name, loaded, total){
834
1278
  },
835
- _onComplete: function(id, fileName, result, xhr){
836
- this._filesInProgress--;
837
- this._maybeParseAndSendUploadError(id, fileName, result, xhr);
1279
+ _onComplete: function(id, name, result, xhr) {
1280
+ if (!result.success) {
1281
+ this._netFilesUploadedOrQueued--;
1282
+ }
1283
+
1284
+ this._removeFromFilesInProgress(id);
1285
+ this._maybeParseAndSendUploadError(id, name, result, xhr);
838
1286
  },
839
- _onCancel: function(id, fileName){
1287
+ _onCancel: function(id, name){
1288
+ this._netFilesUploadedOrQueued--;
1289
+
1290
+ this._removeFromFilesInProgress(id);
1291
+
840
1292
  clearTimeout(this._retryTimeouts[id]);
841
1293
 
842
- var storedFileIndex = qq.indexOf(this._storedFileIds, id);
843
- if (this._options.autoUpload || storedFileIndex < 0) {
844
- this._filesInProgress--;
1294
+ var storedItemIndex = qq.indexOf(this._storedIds, id);
1295
+ if (!this._options.autoUpload && storedItemIndex >= 0) {
1296
+ this._storedIds.splice(storedItemIndex, 1);
1297
+ }
1298
+ },
1299
+ _isDeletePossible: function() {
1300
+ return (this._options.deleteFile.enabled &&
1301
+ (!this._options.cors.expected ||
1302
+ (this._options.cors.expected && (qq.ie10() || !qq.ie()))
1303
+ )
1304
+ );
1305
+ },
1306
+ _onSubmitDelete: function(id) {
1307
+ if (this._isDeletePossible()) {
1308
+ if (this._options.callbacks.onSubmitDelete(id) !== false) {
1309
+ this._deleteHandler.sendDelete(id, this.getUuid(id));
1310
+ }
1311
+ }
1312
+ else {
1313
+ this.log("Delete request ignored for ID " + id + ", delete feature is disabled or request not possible " +
1314
+ "due to CORS on a user agent that does not support pre-flighting.", "warn");
1315
+ return false;
1316
+ }
1317
+ },
1318
+ _onDelete: function(fileId) {},
1319
+ _onDeleteComplete: function(id, xhr, isError) {
1320
+ var name = this._handler.getName(id);
1321
+
1322
+ if (isError) {
1323
+ this.log("Delete request for '" + name + "' has failed.", "error");
1324
+ this._options.callbacks.onError(id, name, "Delete request failed with response code " + xhr.status, xhr);
845
1325
  }
846
- else if (!this._options.autoUpload) {
847
- this._storedFileIds.splice(storedFileIndex, 1);
1326
+ else {
1327
+ this._netFilesUploadedOrQueued--;
1328
+ this.log("Delete request for '" + name + "' has succeeded.");
848
1329
  }
849
1330
  },
850
- _onUpload: function(id, fileName, xhr){
1331
+ _removeFromFilesInProgress: function(id) {
1332
+ var index = qq.indexOf(this._filesInProgress, id);
1333
+ if (index >= 0) {
1334
+ this._filesInProgress.splice(index, 1);
1335
+ }
851
1336
  },
1337
+ _onUpload: function(id, name){},
852
1338
  _onInputChange: function(input){
853
- if (this._handler instanceof qq.UploadHandlerXhr){
1339
+ if (qq.isXhrUploadSupported()){
854
1340
  this.addFiles(input.files);
855
1341
  } else {
856
- if (this._validateFile(input)){
857
- this.addFiles(input);
858
- }
1342
+ this.addFiles(input);
859
1343
  }
860
1344
  this._button.reset();
861
1345
  },
862
- _onBeforeAutoRetry: function(id, fileName) {
863
- this.log("Waiting " + this._options.retry.autoAttemptDelay + " seconds before retrying " + fileName + "...");
1346
+ _onBeforeAutoRetry: function(id, name) {
1347
+ this.log("Waiting " + this._options.retry.autoAttemptDelay + " seconds before retrying " + name + "...");
864
1348
  },
865
- _onAutoRetry: function(id, fileName, responseJSON) {
866
- this.log("Retrying " + fileName + "...");
1349
+ _onAutoRetry: function(id, name, responseJSON) {
1350
+ this.log("Retrying " + name + "...");
867
1351
  this._autoRetries[id]++;
868
1352
  this._handler.retry(id);
869
1353
  },
870
- _shouldAutoRetry: function(id, fileName, responseJSON) {
1354
+ _shouldAutoRetry: function(id, name, responseJSON) {
871
1355
  if (!this._preventRetries[id] && this._options.retry.enableAuto) {
872
1356
  if (this._autoRetries[id] === undefined) {
873
1357
  this._autoRetries[id] = 0;
874
1358
  }
875
1359
 
876
- return this._autoRetries[id] < this._options.retry.maxAutoAttempts
1360
+ return this._autoRetries[id] < this._options.retry.maxAutoAttempts;
877
1361
  }
878
1362
 
879
1363
  return false;
880
1364
  },
881
1365
  //return false if we should not attempt the requested retry
882
1366
  _onBeforeManualRetry: function(id) {
1367
+ var itemLimit = this._options.validation.itemLimit;
1368
+
883
1369
  if (this._preventRetries[id]) {
884
1370
  this.log("Retries are forbidden for id " + id, 'warn');
885
1371
  return false;
@@ -891,8 +1377,13 @@ qq.FineUploaderBasic.prototype = {
891
1377
  return false;
892
1378
  }
893
1379
 
1380
+ if (itemLimit > 0 && this._netFilesUploadedOrQueued+1 > itemLimit) {
1381
+ this._itemError("retryFailTooManyItems", "");
1382
+ return false;
1383
+ }
1384
+
894
1385
  this.log("Retrying upload for '" + fileName + "' (id: " + id + ")...");
895
- this._filesInProgress++;
1386
+ this._filesInProgress.push(id);
896
1387
  return true;
897
1388
  }
898
1389
  else {
@@ -900,29 +1391,28 @@ qq.FineUploaderBasic.prototype = {
900
1391
  return false;
901
1392
  }
902
1393
  },
903
- _maybeParseAndSendUploadError: function(id, fileName, response, xhr) {
1394
+ _maybeParseAndSendUploadError: function(id, name, response, xhr) {
904
1395
  //assuming no one will actually set the response code to something other than 200 and still set 'success' to true
905
1396
  if (!response.success){
906
1397
  if (xhr && xhr.status !== 200 && !response.error) {
907
- this._options.callbacks.onError(id, fileName, "XHR returned response code " + xhr.status);
1398
+ this._options.callbacks.onError(id, name, "XHR returned response code " + xhr.status, xhr);
908
1399
  }
909
1400
  else {
910
1401
  var errorReason = response.error ? response.error : "Upload failure reason unknown";
911
- this._options.callbacks.onError(id, fileName, errorReason);
1402
+ this._options.callbacks.onError(id, name, errorReason, xhr);
912
1403
  }
913
1404
  }
914
1405
  },
915
- _uploadFileList: function(files){
916
- var validationDescriptors, index, batchInvalid;
917
-
918
- validationDescriptors = this._getValidationDescriptors(files);
919
- batchInvalid = this._options.callbacks.onValidate(validationDescriptors) === false;
920
-
921
- if (!batchInvalid) {
922
- if (files.length > 0) {
923
- for (index = 0; index < files.length; index++){
924
- if (this._validateFile(files[index])){
925
- this._uploadFile(files[index]);
1406
+ _uploadFileOrBlobDataList: function(fileOrBlobDataList){
1407
+ var index,
1408
+ validationDescriptors = this._getValidationDescriptors(fileOrBlobDataList),
1409
+ batchValid = this._isBatchValid(validationDescriptors);
1410
+
1411
+ if (batchValid) {
1412
+ if (fileOrBlobDataList.length > 0) {
1413
+ for (index = 0; index < fileOrBlobDataList.length; index++){
1414
+ if (this._validateFileOrBlobData(fileOrBlobDataList[index])){
1415
+ this._upload(fileOrBlobDataList[index]);
926
1416
  } else {
927
1417
  if (this._options.validation.stopOnFirstInvalidFile){
928
1418
  return;
@@ -931,94 +1421,141 @@ qq.FineUploaderBasic.prototype = {
931
1421
  }
932
1422
  }
933
1423
  else {
934
- this._error('noFilesError', "");
1424
+ this._itemError("noFilesError", "");
935
1425
  }
936
1426
  }
937
1427
  },
938
- _uploadFile: function(fileContainer){
939
- var id = this._handler.add(fileContainer);
940
- var fileName = this._handler.getName(id);
1428
+ _upload: function(blobOrFileContainer){
1429
+ var id = this._handler.add(blobOrFileContainer);
1430
+ var name = this._handler.getName(id);
1431
+
1432
+ if (this._options.callbacks.onSubmit(id, name) !== false) {
1433
+ this._onSubmit(id, name);
1434
+ this._options.callbacks.onSubmitted(id, name);
941
1435
 
942
- if (this._options.callbacks.onSubmit(id, fileName) !== false){
943
- this._onSubmit(id, fileName);
944
1436
  if (this._options.autoUpload) {
945
1437
  this._handler.upload(id);
946
1438
  }
947
1439
  else {
948
- this._storeFileForLater(id);
1440
+ this._storeForLater(id);
949
1441
  }
950
1442
  }
951
1443
  },
952
- _storeFileForLater: function(id) {
953
- this._storedFileIds.push(id);
1444
+ _storeForLater: function(id) {
1445
+ this._storedIds.push(id);
1446
+ },
1447
+ _isBatchValid: function(validationDescriptors) {
1448
+ //first, defer the check to the callback (ask the integrator)
1449
+ var errorMessage,
1450
+ itemLimit = this._options.validation.itemLimit,
1451
+ proposedNetFilesUploadedOrQueued = this._netFilesUploadedOrQueued + validationDescriptors.length,
1452
+ batchValid = this._options.callbacks.onValidateBatch(validationDescriptors) !== false;
1453
+
1454
+ //if the callback hasn't rejected the batch, run some internal tests on the batch next
1455
+ if (batchValid) {
1456
+ if (itemLimit === 0 || proposedNetFilesUploadedOrQueued <= itemLimit) {
1457
+ batchValid = true;
1458
+ }
1459
+ else {
1460
+ batchValid = false;
1461
+ errorMessage = this._options.messages.tooManyItemsError
1462
+ .replace(/\{netItems\}/g, proposedNetFilesUploadedOrQueued)
1463
+ .replace(/\{itemLimit\}/g, itemLimit);
1464
+ this._batchError(errorMessage);
1465
+ }
1466
+ }
1467
+
1468
+ return batchValid;
954
1469
  },
955
- _validateFile: function(file){
1470
+ _validateFileOrBlobData: function(fileOrBlobData){
956
1471
  var validationDescriptor, name, size;
957
1472
 
958
- validationDescriptor = this._getValidationDescriptor(file);
1473
+ validationDescriptor = this._getValidationDescriptor(fileOrBlobData);
959
1474
  name = validationDescriptor.name;
960
1475
  size = validationDescriptor.size;
961
1476
 
962
- if (this._options.callbacks.onValidate([validationDescriptor]) === false) {
1477
+ if (this._options.callbacks.onValidate(validationDescriptor) === false) {
963
1478
  return false;
964
1479
  }
965
1480
 
966
- if (!this._isAllowedExtension(name)){
967
- this._error('typeError', name);
1481
+ if (qq.isFileOrInput(fileOrBlobData) && !this._isAllowedExtension(name)){
1482
+ this._itemError('typeError', name);
968
1483
  return false;
969
1484
 
970
1485
  }
971
1486
  else if (size === 0){
972
- this._error('emptyError', name);
1487
+ this._itemError('emptyError', name);
973
1488
  return false;
974
1489
 
975
1490
  }
976
1491
  else if (size && this._options.validation.sizeLimit && size > this._options.validation.sizeLimit){
977
- this._error('sizeError', name);
1492
+ this._itemError('sizeError', name);
978
1493
  return false;
979
1494
 
980
1495
  }
981
1496
  else if (size && size < this._options.validation.minSizeLimit){
982
- this._error('minSizeError', name);
1497
+ this._itemError('minSizeError', name);
983
1498
  return false;
984
1499
  }
985
1500
 
986
1501
  return true;
987
1502
  },
988
- _error: function(code, fileName){
989
- var message = this._options.messages[code];
1503
+ _itemError: function(code, name) {
1504
+ var message = this._options.messages[code],
1505
+ allowedExtensions = [],
1506
+ extensionsForMessage;
1507
+
990
1508
  function r(name, replacement){ message = message.replace(name, replacement); }
991
1509
 
992
- var extensions = this._options.validation.allowedExtensions.join(', ');
1510
+ qq.each(this._options.validation.allowedExtensions, function(idx, allowedExtension) {
1511
+ /**
1512
+ * If an argument is not a string, ignore it. Added when a possible issue with MooTools hijacking the
1513
+ * `allowedExtensions` array was discovered. See case #735 in the issue tracker for more details.
1514
+ */
1515
+ if (qq.isString(allowedExtension)) {
1516
+ allowedExtensions.push(allowedExtension);
1517
+ }
1518
+ });
1519
+
1520
+ extensionsForMessage = allowedExtensions.join(', ').toLowerCase();
993
1521
 
994
- r('{file}', this._formatFileName(fileName));
995
- r('{extensions}', extensions);
1522
+ r('{file}', this._options.formatFileName(name));
1523
+ r('{extensions}', extensionsForMessage);
996
1524
  r('{sizeLimit}', this._formatSize(this._options.validation.sizeLimit));
997
1525
  r('{minSizeLimit}', this._formatSize(this._options.validation.minSizeLimit));
998
1526
 
999
- this._options.callbacks.onError(null, fileName, message);
1527
+ this._options.callbacks.onError(null, name, message);
1000
1528
 
1001
1529
  return message;
1002
1530
  },
1003
- _formatFileName: function(name){
1004
- if (name.length > 33){
1005
- name = name.slice(0, 19) + '...' + name.slice(-13);
1006
- }
1007
- return name;
1531
+ _batchError: function(message) {
1532
+ this._options.callbacks.onError(null, null, message);
1008
1533
  },
1009
1534
  _isAllowedExtension: function(fileName){
1010
- var ext = (-1 !== fileName.indexOf('.'))
1011
- ? fileName.replace(/.*[.]/, '').toLowerCase()
1012
- : '';
1013
- var allowed = this._options.validation.allowedExtensions;
1535
+ var allowed = this._options.validation.allowedExtensions,
1536
+ valid = false;
1014
1537
 
1015
- if (!allowed.length){return true;}
1016
-
1017
- for (var i=0; i<allowed.length; i++){
1018
- if (allowed[i].toLowerCase() == ext){ return true;}
1538
+ if (!allowed.length) {
1539
+ return true;
1019
1540
  }
1020
1541
 
1021
- return false;
1542
+ qq.each(allowed, function(idx, allowedExt) {
1543
+ /**
1544
+ * If an argument is not a string, ignore it. Added when a possible issue with MooTools hijacking the
1545
+ * `allowedExtensions` array was discovered. See case #735 in the issue tracker for more details.
1546
+ */
1547
+ if (qq.isString(allowedExt)) {
1548
+ /*jshint eqeqeq: true, eqnull: true*/
1549
+ var extRegex = new RegExp('\\.' + allowedExt + "$", 'i');
1550
+
1551
+ if (fileName.match(extRegex) != null) {
1552
+ valid = true;
1553
+ return false;
1554
+ }
1555
+ }
1556
+ });
1557
+
1558
+ return valid;
1022
1559
  },
1023
1560
  _formatSize: function(bytes){
1024
1561
  var i = -1;
@@ -1027,7 +1564,7 @@ qq.FineUploaderBasic.prototype = {
1027
1564
  i++;
1028
1565
  } while (bytes > 99);
1029
1566
 
1030
- return Math.max(bytes, 0.1).toFixed(1) + ['kB', 'MB', 'GB', 'TB', 'PB', 'EB'][i];
1567
+ return Math.max(bytes, 0.1).toFixed(1) + this._options.text.sizeSymbols[i];
1031
1568
  },
1032
1569
  _wrapCallbacks: function() {
1033
1570
  var self, safeCallback;
@@ -1039,9 +1576,9 @@ qq.FineUploaderBasic.prototype = {
1039
1576
  return callback.apply(self, args);
1040
1577
  }
1041
1578
  catch (exception) {
1042
- self.log("Caught exception in '" + name + "' callback - " + exception, 'error');
1579
+ self.log("Caught exception in '" + name + "' callback - " + exception.message, 'error');
1043
1580
  }
1044
- }
1581
+ };
1045
1582
 
1046
1583
  for (var prop in this._options.callbacks) {
1047
1584
  (function() {
@@ -1050,40 +1587,50 @@ qq.FineUploaderBasic.prototype = {
1050
1587
  callbackFunc = self._options.callbacks[callbackName];
1051
1588
  self._options.callbacks[callbackName] = function() {
1052
1589
  return safeCallback(callbackName, callbackFunc, arguments);
1053
- }
1590
+ };
1054
1591
  }());
1055
1592
  }
1056
1593
  },
1057
- _parseFileName: function(file) {
1594
+ _parseFileOrBlobDataName: function(fileOrBlobData) {
1058
1595
  var name;
1059
1596
 
1060
- if (file.value){
1061
- // it is a file input
1062
- // get input value and remove path to normalize
1063
- name = file.value.replace(/.*(\/|\\)/, "");
1064
- } else {
1065
- // fix missing properties in Safari 4 and firefox 11.0a2
1066
- name = (file.fileName !== null && file.fileName !== undefined) ? file.fileName : file.name;
1597
+ if (qq.isFileOrInput(fileOrBlobData)) {
1598
+ if (fileOrBlobData.value) {
1599
+ // it is a file input
1600
+ // get input value and remove path to normalize
1601
+ name = fileOrBlobData.value.replace(/.*(\/|\\)/, "");
1602
+ } else {
1603
+ // fix missing properties in Safari 4 and firefox 11.0a2
1604
+ name = (fileOrBlobData.fileName !== null && fileOrBlobData.fileName !== undefined) ? fileOrBlobData.fileName : fileOrBlobData.name;
1605
+ }
1606
+ }
1607
+ else {
1608
+ name = fileOrBlobData.name;
1067
1609
  }
1068
1610
 
1069
1611
  return name;
1070
1612
  },
1071
- _parseFileSize: function(file) {
1613
+ _parseFileOrBlobDataSize: function(fileOrBlobData) {
1072
1614
  var size;
1073
1615
 
1074
- if (!file.value){
1075
- // fix missing properties in Safari 4 and firefox 11.0a2
1076
- size = (file.fileSize !== null && file.fileSize !== undefined) ? file.fileSize : file.size;
1616
+ if (qq.isFileOrInput(fileOrBlobData)) {
1617
+ if (!fileOrBlobData.value){
1618
+ // fix missing properties in Safari 4 and firefox 11.0a2
1619
+ size = (fileOrBlobData.fileSize !== null && fileOrBlobData.fileSize !== undefined) ? fileOrBlobData.fileSize : fileOrBlobData.size;
1620
+ }
1621
+ }
1622
+ else {
1623
+ size = fileOrBlobData.blob.size;
1077
1624
  }
1078
1625
 
1079
1626
  return size;
1080
1627
  },
1081
- _getValidationDescriptor: function(file) {
1628
+ _getValidationDescriptor: function(fileOrBlobData) {
1082
1629
  var name, size, fileDescriptor;
1083
1630
 
1084
1631
  fileDescriptor = {};
1085
- name = this._parseFileName(file);
1086
- size = this._parseFileSize(file);
1632
+ name = this._parseFileOrBlobDataName(fileOrBlobData);
1633
+ size = this._parseFileOrBlobDataSize(fileOrBlobData);
1087
1634
 
1088
1635
  fileDescriptor.name = name;
1089
1636
  if (size) {
@@ -1093,35 +1640,35 @@ qq.FineUploaderBasic.prototype = {
1093
1640
  return fileDescriptor;
1094
1641
  },
1095
1642
  _getValidationDescriptors: function(files) {
1096
- var index, fileDescriptors;
1097
-
1098
- fileDescriptors = [];
1643
+ var self = this,
1644
+ fileDescriptors = [];
1099
1645
 
1100
- for (index = 0; index < files.length; index++) {
1101
- fileDescriptors.push(files[index]);
1102
- }
1646
+ qq.each(files, function(idx, file) {
1647
+ fileDescriptors.push(self._getValidationDescriptor(file));
1648
+ });
1103
1649
 
1104
1650
  return fileDescriptors;
1105
1651
  },
1106
- _createParamsStore: function() {
1652
+ _createParamsStore: function(type) {
1107
1653
  var paramsStore = {},
1108
1654
  self = this;
1109
1655
 
1110
1656
  return {
1111
- setParams: function(params, fileId) {
1657
+ setParams: function(params, id) {
1112
1658
  var paramsCopy = {};
1113
1659
  qq.extend(paramsCopy, params);
1114
- paramsStore[fileId] = paramsCopy;
1660
+ paramsStore[id] = paramsCopy;
1115
1661
  },
1116
1662
 
1117
- getParams: function(fileId) {
1663
+ getParams: function(id) {
1664
+ /*jshint eqeqeq: true, eqnull: true*/
1118
1665
  var paramsCopy = {};
1119
1666
 
1120
- if (fileId !== undefined && paramsStore[fileId]) {
1121
- qq.extend(paramsCopy, paramsStore[fileId]);
1667
+ if (id != null && paramsStore[id]) {
1668
+ qq.extend(paramsCopy, paramsStore[id]);
1122
1669
  }
1123
1670
  else {
1124
- qq.extend(paramsCopy, self._options.request.params);
1671
+ qq.extend(paramsCopy, self._options[type].params);
1125
1672
  }
1126
1673
 
1127
1674
  return paramsCopy;
@@ -1134,7 +1681,34 @@ qq.FineUploaderBasic.prototype = {
1134
1681
  reset: function() {
1135
1682
  paramsStore = {};
1136
1683
  }
1137
- }
1684
+ };
1685
+ },
1686
+ _createEndpointStore: function(type) {
1687
+ var endpointStore = {},
1688
+ self = this;
1689
+
1690
+ return {
1691
+ setEndpoint: function(endpoint, id) {
1692
+ endpointStore[id] = endpoint;
1693
+ },
1694
+
1695
+ getEndpoint: function(id) {
1696
+ /*jshint eqeqeq: true, eqnull: true*/
1697
+ if (id != null && endpointStore[id]) {
1698
+ return endpointStore[id];
1699
+ }
1700
+
1701
+ return self._options[type].endpoint;
1702
+ },
1703
+
1704
+ remove: function(fileId) {
1705
+ return delete endpointStore[fileId];
1706
+ },
1707
+
1708
+ reset: function() {
1709
+ endpointStore = {};
1710
+ }
1711
+ };
1138
1712
  }
1139
1713
  };
1140
1714
  /*globals qq, document*/
@@ -1212,7 +1786,9 @@ qq.DragAndDrop = function(o) {
1212
1786
  dz.dropDisabled(true);
1213
1787
 
1214
1788
  if (dataTransfer.files.length > 1 && !options.multiple) {
1789
+ options.callbacks.dropProcessing(false);
1215
1790
  options.callbacks.error('tooManyFilesError', "");
1791
+ dz.dropDisabled(false);
1216
1792
  }
1217
1793
  else {
1218
1794
  droppedFiles = [];
@@ -1513,6 +2089,7 @@ qq.FineUploader = function(o){
1513
2089
  uploadButton: 'Upload a file',
1514
2090
  cancelButton: 'Cancel',
1515
2091
  retryButton: 'Retry',
2092
+ deleteButton: 'Delete',
1516
2093
  failUpload: 'Upload failed',
1517
2094
  dragZone: 'Drop files here to upload',
1518
2095
  dropProcessing: 'Processing dropped files...',
@@ -1535,6 +2112,7 @@ qq.FineUploader = function(o){
1535
2112
  '<span class="qq-upload-size"></span>' +
1536
2113
  '<a class="qq-upload-cancel" href="#">{cancelButtonText}</a>' +
1537
2114
  '<a class="qq-upload-retry" href="#">{retryButtonText}</a>' +
2115
+ '<a class="qq-upload-delete" href="#">{deleteButtonText}</a>' +
1538
2116
  '<span class="qq-upload-status-text">{statusText}</span>' +
1539
2117
  '</li>',
1540
2118
  classes: {
@@ -1551,6 +2129,7 @@ qq.FineUploader = function(o){
1551
2129
  retryable: 'qq-upload-retryable',
1552
2130
  size: 'qq-upload-size',
1553
2131
  cancel: 'qq-upload-cancel',
2132
+ deleteButton: 'qq-upload-delete',
1554
2133
  retry: 'qq-upload-retry',
1555
2134
  statusText: 'qq-upload-status-text',
1556
2135
 
@@ -1577,8 +2156,49 @@ qq.FineUploader = function(o){
1577
2156
  autoRetryNote: "Retrying {retryNum}/{maxAuto}...",
1578
2157
  showButton: false
1579
2158
  },
2159
+ deleteFile: {
2160
+ forceConfirm: false,
2161
+ confirmMessage: "Are you sure you want to delete {filename}?",
2162
+ deletingStatusText: "Deleting...",
2163
+ deletingFailedText: "Delete failed"
2164
+
2165
+ },
2166
+ display: {
2167
+ fileSizeOnSubmit: false
2168
+ },
2169
+ paste: {
2170
+ promptForName: false,
2171
+ namePromptMessage: "Please name this image"
2172
+ },
1580
2173
  showMessage: function(message){
1581
- alert(message);
2174
+ setTimeout(function() {
2175
+ window.alert(message);
2176
+ }, 0);
2177
+ },
2178
+ showConfirm: function(message, okCallback, cancelCallback) {
2179
+ setTimeout(function() {
2180
+ var result = window.confirm(message);
2181
+ if (result) {
2182
+ okCallback();
2183
+ }
2184
+ else if (cancelCallback) {
2185
+ cancelCallback();
2186
+ }
2187
+ }, 0);
2188
+ },
2189
+ showPrompt: function(message, defaultValue) {
2190
+ var promise = new qq.Promise(),
2191
+ retVal = window.prompt(message, defaultValue);
2192
+
2193
+ /*jshint eqeqeq: true, eqnull: true*/
2194
+ if (retVal != null && qq.trimStr(retVal).length > 0) {
2195
+ promise.success(retVal);
2196
+ }
2197
+ else {
2198
+ promise.failure("Undefined or invalid user-supplied value.");
2199
+ }
2200
+
2201
+ return promise;
1582
2202
  }
1583
2203
  }, true);
1584
2204
 
@@ -1593,6 +2213,7 @@ qq.FineUploader = function(o){
1593
2213
  this._options.template = this._options.template.replace(/\{dropProcessingText\}/g, this._options.text.dropProcessing);
1594
2214
  this._options.fileTemplate = this._options.fileTemplate.replace(/\{cancelButtonText\}/g, this._options.text.cancelButton);
1595
2215
  this._options.fileTemplate = this._options.fileTemplate.replace(/\{retryButtonText\}/g, this._options.text.retryButton);
2216
+ this._options.fileTemplate = this._options.fileTemplate.replace(/\{deleteButtonText\}/g, this._options.text.deleteButton);
1596
2217
  this._options.fileTemplate = this._options.fileTemplate.replace(/\{statusText\}/g, "");
1597
2218
 
1598
2219
  this._element = this._options.element;
@@ -1608,6 +2229,10 @@ qq.FineUploader = function(o){
1608
2229
  this._bindCancelAndRetryEvents();
1609
2230
 
1610
2231
  this._dnd = this._setupDragAndDrop();
2232
+
2233
+ if (this._options.paste.targetElement && this._options.paste.promptForName) {
2234
+ this._setupPastePrompt();
2235
+ }
1611
2236
  };
1612
2237
 
1613
2238
  // inherit from Basic Uploader
@@ -1634,11 +2259,6 @@ qq.extend(qq.FineUploader.prototype, {
1634
2259
  item = item.nextSibling;
1635
2260
  }
1636
2261
  },
1637
- cancel: function(fileId) {
1638
- qq.FineUploaderBasic.prototype.cancel.apply(this, arguments);
1639
- var item = this.getItemByFileId(fileId);
1640
- qq(item).remove();
1641
- },
1642
2262
  reset: function() {
1643
2263
  qq.FineUploaderBasic.prototype.reset.apply(this, arguments);
1644
2264
  this._element.innerHTML = this._options.template;
@@ -1650,6 +2270,10 @@ qq.extend(qq.FineUploader.prototype, {
1650
2270
  this._dnd.dispose();
1651
2271
  this._dnd = this._setupDragAndDrop();
1652
2272
  },
2273
+ _removeFileItem: function(fileId) {
2274
+ var item = this.getItemByFileId(fileId);
2275
+ qq(item).remove();
2276
+ },
1653
2277
  _setupDragAndDrop: function() {
1654
2278
  var self = this,
1655
2279
  dropProcessingEl = this._find(this._element, 'dropProcessing'),
@@ -1689,7 +2313,7 @@ qq.extend(qq.FineUploader.prototype, {
1689
2313
  }
1690
2314
  },
1691
2315
  error: function(code, filename) {
1692
- self._error(code, filename);
2316
+ self._itemError(code, filename);
1693
2317
  },
1694
2318
  log: function(message, level) {
1695
2319
  self.log(message, level);
@@ -1705,8 +2329,8 @@ qq.extend(qq.FineUploader.prototype, {
1705
2329
  return ((qq.chrome() || (qq.safari() && qq.windows())) && e.clientX == 0 && e.clientY == 0) // null coords for Chrome and Safari Windows
1706
2330
  || (qq.firefox() && !e.relatedTarget); // null e.relatedTarget for Firefox
1707
2331
  },
1708
- _storeFileForLater: function(id) {
1709
- qq.FineUploaderBasic.prototype._storeFileForLater.apply(this, arguments);
2332
+ _storeForLater: function(id) {
2333
+ qq.FineUploaderBasic.prototype._storeForLater.apply(this, arguments);
1710
2334
  var item = this.getItemByFileId(id);
1711
2335
  qq(this._find(item, 'spinner')).hide();
1712
2336
  },
@@ -1721,15 +2345,15 @@ qq.extend(qq.FineUploader.prototype, {
1721
2345
 
1722
2346
  return element;
1723
2347
  },
1724
- _onSubmit: function(id, fileName){
2348
+ _onSubmit: function(id, name){
1725
2349
  qq.FineUploaderBasic.prototype._onSubmit.apply(this, arguments);
1726
- this._addToList(id, fileName);
2350
+ this._addToList(id, name);
1727
2351
  },
1728
2352
  // Update the progress bar & percentage as the file is uploaded
1729
- _onProgress: function(id, fileName, loaded, total){
2353
+ _onProgress: function(id, name, loaded, total){
1730
2354
  qq.FineUploaderBasic.prototype._onProgress.apply(this, arguments);
1731
2355
 
1732
- var item, progressBar, text, percent, cancelLink, size;
2356
+ var item, progressBar, percent, cancelLink;
1733
2357
 
1734
2358
  item = this.getItemByFileId(id);
1735
2359
  progressBar = this._find(item, 'progressBar');
@@ -1742,24 +2366,20 @@ qq.extend(qq.FineUploader.prototype, {
1742
2366
  qq(progressBar).hide();
1743
2367
  qq(this._find(item, 'statusText')).setText(this._options.text.waitingForResponse);
1744
2368
 
1745
- // If last byte was sent, just display final size
1746
- text = this._formatSize(total);
2369
+ // If last byte was sent, display total file size
2370
+ this._displayFileSize(id);
1747
2371
  }
1748
2372
  else {
1749
- // If still uploading, display percentage
1750
- text = this._formatProgress(loaded, total);
2373
+ // If still uploading, display percentage - total size is actually the total request(s) size
2374
+ this._displayFileSize(id, loaded, total);
1751
2375
 
1752
2376
  qq(progressBar).css({display: 'block'});
1753
2377
  }
1754
2378
 
1755
2379
  // Update progress bar element
1756
2380
  qq(progressBar).css({width: percent + '%'});
1757
-
1758
- size = this._find(item, 'size');
1759
- qq(size).css({display: 'inline'});
1760
- qq(size).setText(text);
1761
2381
  },
1762
- _onComplete: function(id, fileName, result, xhr){
2382
+ _onComplete: function(id, name, result, xhr){
1763
2383
  qq.FineUploaderBasic.prototype._onComplete.apply(this, arguments);
1764
2384
 
1765
2385
  var item = this.getItemByFileId(id);
@@ -1774,7 +2394,11 @@ qq.extend(qq.FineUploader.prototype, {
1774
2394
  }
1775
2395
  qq(this._find(item, 'spinner')).hide();
1776
2396
 
1777
- if (result.success){
2397
+ if (result.success) {
2398
+ if (this._isDeletePossible()) {
2399
+ this._showDeleteLink(id);
2400
+ }
2401
+
1778
2402
  qq(item).addClass(this._classes.success);
1779
2403
  if (this._classes.successIcon) {
1780
2404
  this._find(item, 'finished').style.display = "inline-block";
@@ -1792,14 +2416,17 @@ qq.extend(qq.FineUploader.prototype, {
1792
2416
  this._controlFailureTextDisplay(item, result);
1793
2417
  }
1794
2418
  },
1795
- _onUpload: function(id, fileName, xhr){
2419
+ _onUpload: function(id, name){
1796
2420
  qq.FineUploaderBasic.prototype._onUpload.apply(this, arguments);
1797
2421
 
1798
- var item = this.getItemByFileId(id);
1799
- this._showSpinner(item);
2422
+ this._showSpinner(id);
2423
+ },
2424
+ _onCancel: function(id, name) {
2425
+ qq.FineUploaderBasic.prototype._onCancel.apply(this, arguments);
2426
+ this._removeFileItem(id);
1800
2427
  },
1801
2428
  _onBeforeAutoRetry: function(id) {
1802
- var item, progressBar, cancelLink, failTextEl, retryNumForDisplay, maxAuto, retryNote;
2429
+ var item, progressBar, failTextEl, retryNumForDisplay, maxAuto, retryNote;
1803
2430
 
1804
2431
  qq.FineUploaderBasic.prototype._onBeforeAutoRetry.apply(this, arguments);
1805
2432
 
@@ -1826,17 +2453,75 @@ qq.extend(qq.FineUploader.prototype, {
1826
2453
  },
1827
2454
  //return false if we should not attempt the requested retry
1828
2455
  _onBeforeManualRetry: function(id) {
2456
+ var item = this.getItemByFileId(id);
2457
+
1829
2458
  if (qq.FineUploaderBasic.prototype._onBeforeManualRetry.apply(this, arguments)) {
1830
- var item = this.getItemByFileId(id);
1831
2459
  this._find(item, 'progressBar').style.width = 0;
1832
2460
  qq(item).removeClass(this._classes.fail);
1833
- this._showSpinner(item);
2461
+ qq(this._find(item, 'statusText')).clearText();
2462
+ this._showSpinner(id);
1834
2463
  this._showCancelLink(item);
1835
2464
  return true;
1836
2465
  }
1837
- return false;
2466
+ else {
2467
+ qq(item).addClass(this._classes.retryable);
2468
+ return false;
2469
+ }
2470
+ },
2471
+ _onSubmitDelete: function(id) {
2472
+ if (this._isDeletePossible()) {
2473
+ if (this._options.callbacks.onSubmitDelete(id) !== false) {
2474
+ if (this._options.deleteFile.forceConfirm) {
2475
+ this._showDeleteConfirm(id);
2476
+ }
2477
+ else {
2478
+ this._sendDeleteRequest(id);
2479
+ }
2480
+ }
2481
+ }
2482
+ else {
2483
+ this.log("Delete request ignored for file ID " + id + ", delete feature is disabled.", "warn");
2484
+ return false;
2485
+ }
2486
+ },
2487
+ _onDeleteComplete: function(id, xhr, isError) {
2488
+ qq.FineUploaderBasic.prototype._onDeleteComplete.apply(this, arguments);
2489
+
2490
+ var item = this.getItemByFileId(id),
2491
+ spinnerEl = this._find(item, 'spinner'),
2492
+ statusTextEl = this._find(item, 'statusText');
2493
+
2494
+ qq(spinnerEl).hide();
2495
+
2496
+ if (isError) {
2497
+ qq(statusTextEl).setText(this._options.deleteFile.deletingFailedText);
2498
+ this._showDeleteLink(id);
2499
+ }
2500
+ else {
2501
+ this._removeFileItem(id);
2502
+ }
1838
2503
  },
1839
- _addToList: function(id, fileName){
2504
+ _sendDeleteRequest: function(id) {
2505
+ var item = this.getItemByFileId(id),
2506
+ deleteLink = this._find(item, 'deleteButton'),
2507
+ statusTextEl = this._find(item, 'statusText');
2508
+
2509
+ qq(deleteLink).hide();
2510
+ this._showSpinner(id);
2511
+ qq(statusTextEl).setText(this._options.deleteFile.deletingStatusText);
2512
+ this._deleteHandler.sendDelete(id, this.getUuid(id));
2513
+ },
2514
+ _showDeleteConfirm: function(id) {
2515
+ var fileName = this._handler.getName(id),
2516
+ confirmMessage = this._options.deleteFile.confirmMessage.replace(/\{filename\}/g, fileName),
2517
+ uuid = this.getUuid(id),
2518
+ self = this;
2519
+
2520
+ this._options.showConfirm(confirmMessage, function() {
2521
+ self._sendDeleteRequest(id);
2522
+ });
2523
+ },
2524
+ _addToList: function(id, name){
1840
2525
  var item = qq.toElement(this._options.fileTemplate);
1841
2526
  if (this._options.disableCancelForFormUploads && !qq.isXhrUploadSupported()) {
1842
2527
  var cancelLink = this._find(item, 'cancel');
@@ -1846,15 +2531,36 @@ qq.extend(qq.FineUploader.prototype, {
1846
2531
  item.qqFileId = id;
1847
2532
 
1848
2533
  var fileElement = this._find(item, 'file');
1849
- qq(fileElement).setText(this._formatFileName(fileName));
2534
+ qq(fileElement).setText(this._options.formatFileName(name));
1850
2535
  qq(this._find(item, 'size')).hide();
1851
- if (!this._options.multiple) this._clearList();
2536
+ if (!this._options.multiple) {
2537
+ this._handler.cancelAll();
2538
+ this._clearList();
2539
+ }
2540
+
1852
2541
  this._listElement.appendChild(item);
2542
+
2543
+ if (this._options.display.fileSizeOnSubmit && qq.isXhrUploadSupported()) {
2544
+ this._displayFileSize(id);
2545
+ }
1853
2546
  },
1854
2547
  _clearList: function(){
1855
2548
  this._listElement.innerHTML = '';
1856
2549
  this.clearStoredFiles();
1857
2550
  },
2551
+ _displayFileSize: function(id, loadedSize, totalSize) {
2552
+ var item = this.getItemByFileId(id),
2553
+ size = this.getSize(id),
2554
+ sizeForDisplay = this._formatSize(size),
2555
+ sizeEl = this._find(item, 'size');
2556
+
2557
+ if (loadedSize !== undefined && totalSize !== undefined) {
2558
+ sizeForDisplay = this._formatProgress(loadedSize, totalSize);
2559
+ }
2560
+
2561
+ qq(sizeEl).css({display: 'inline'});
2562
+ qq(sizeEl).setText(sizeForDisplay);
2563
+ },
1858
2564
  /**
1859
2565
  * delegate click event for cancel & retry links
1860
2566
  **/
@@ -1866,15 +2572,18 @@ qq.extend(qq.FineUploader.prototype, {
1866
2572
  e = e || window.event;
1867
2573
  var target = e.target || e.srcElement;
1868
2574
 
1869
- if (qq(target).hasClass(self._classes.cancel) || qq(target).hasClass(self._classes.retry)){
2575
+ if (qq(target).hasClass(self._classes.cancel) || qq(target).hasClass(self._classes.retry) || qq(target).hasClass(self._classes.deleteButton)){
1870
2576
  qq.preventDefault(e);
1871
2577
 
1872
2578
  var item = target.parentNode;
1873
- while(item.qqFileId == undefined) {
2579
+ while(item.qqFileId === undefined) {
1874
2580
  item = target = target.parentNode;
1875
2581
  }
1876
2582
 
1877
- if (qq(target).hasClass(self._classes.cancel)) {
2583
+ if (qq(target).hasClass(self._classes.deleteButton)) {
2584
+ self.deleteFile(item.qqFileId);
2585
+ }
2586
+ else if (qq(target).hasClass(self._classes.cancel)) {
1878
2587
  self.cancel(item.qqFileId);
1879
2588
  }
1880
2589
  else {
@@ -1924,329 +2633,664 @@ qq.extend(qq.FineUploader.prototype, {
1924
2633
  this.log("failedUploadTextDisplay.mode value of '" + mode + "' is not valid", 'warn');
1925
2634
  }
1926
2635
  },
1927
- //TODO turn this into a real tooltip, with click trigger (so it is usable on mobile devices). See case #355 for details.
1928
2636
  _showTooltip: function(item, text) {
1929
2637
  item.title = text;
1930
2638
  },
1931
- _showSpinner: function(item) {
1932
- var spinnerEl = this._find(item, 'spinner');
2639
+ _showSpinner: function(id) {
2640
+ var item = this.getItemByFileId(id),
2641
+ spinnerEl = this._find(item, 'spinner');
2642
+
1933
2643
  spinnerEl.style.display = "inline-block";
1934
2644
  },
1935
2645
  _showCancelLink: function(item) {
1936
2646
  if (!this._options.disableCancelForFormUploads || qq.isXhrUploadSupported()) {
1937
2647
  var cancelLink = this._find(item, 'cancel');
1938
- cancelLink.style.display = 'inline';
2648
+
2649
+ qq(cancelLink).css({display: 'inline'});
1939
2650
  }
1940
2651
  },
1941
- _error: function(code, fileName){
1942
- var message = qq.FineUploaderBasic.prototype._error.apply(this, arguments);
2652
+ _showDeleteLink: function(id) {
2653
+ var item = this.getItemByFileId(id),
2654
+ deleteLink = this._find(item, 'deleteButton');
2655
+
2656
+ qq(deleteLink).css({display: 'inline'});
2657
+ },
2658
+ _itemError: function(code, name){
2659
+ var message = qq.FineUploaderBasic.prototype._itemError.apply(this, arguments);
2660
+ this._options.showMessage(message);
2661
+ },
2662
+ _batchError: function(message) {
2663
+ qq.FineUploaderBasic.prototype._batchError.apply(this, arguments);
1943
2664
  this._options.showMessage(message);
2665
+ },
2666
+ _setupPastePrompt: function() {
2667
+ var self = this;
2668
+
2669
+ this._options.callbacks.onPasteReceived = function() {
2670
+ var message = self._options.paste.namePromptMessage,
2671
+ defaultVal = self._options.paste.defaultName;
2672
+
2673
+ return self._options.showPrompt(message, defaultVal);
2674
+ };
1944
2675
  }
1945
2676
  });
1946
- /**
1947
- * Class for uploading files, uploading itself is handled by child classes
1948
- */
1949
- qq.UploadHandlerAbstract = function(o){
1950
- // Default options, can be overridden by the user
1951
- this._options = {
1952
- debug: false,
1953
- endpoint: '/upload.php',
1954
- paramsInBody: false,
1955
- // maximum number of concurrent uploads
1956
- maxConnections: 999,
1957
- log: function(str, level) {},
1958
- onProgress: function(id, fileName, loaded, total){},
1959
- onComplete: function(id, fileName, response, xhr){},
1960
- onCancel: function(id, fileName){},
1961
- onUpload: function(id, fileName, xhr){},
1962
- onAutoRetry: function(id, fileName, response, xhr){}
2677
+ /** Generic class for sending non-upload ajax requests and handling the associated responses **/
2678
+ //TODO Use XDomainRequest if expectCors = true. Not necessary now since only DELETE requests are sent and XDR doesn't support pre-flighting.
2679
+ /*globals qq, XMLHttpRequest*/
2680
+ qq.AjaxRequestor = function(o) {
2681
+ "use strict";
1963
2682
 
1964
- };
1965
- qq.extend(this._options, o);
2683
+ var log, shouldParamsBeInQueryString,
2684
+ queue = [],
2685
+ requestState = [],
2686
+ options = {
2687
+ method: 'POST',
2688
+ maxConnections: 3,
2689
+ customHeaders: {},
2690
+ endpointStore: {},
2691
+ paramsStore: {},
2692
+ successfulResponseCodes: [200],
2693
+ demoMode: false,
2694
+ cors: {
2695
+ expected: false,
2696
+ sendCredentials: false
2697
+ },
2698
+ log: function(str, level) {},
2699
+ onSend: function(id) {},
2700
+ onComplete: function(id, xhr, isError) {},
2701
+ onCancel: function(id) {}
2702
+ };
1966
2703
 
1967
- this._queue = [];
2704
+ qq.extend(options, o);
2705
+ log = options.log;
2706
+ shouldParamsBeInQueryString = getMethod() === 'GET' || getMethod() === 'DELETE';
1968
2707
 
1969
- this.log = this._options.log;
1970
- };
1971
- qq.UploadHandlerAbstract.prototype = {
1972
- /**
1973
- * Adds file or file input to the queue
1974
- * @returns id
1975
- **/
1976
- add: function(file){},
1977
- /**
1978
- * Sends the file identified by id
1979
- */
1980
- upload: function(id){
1981
- var len = this._queue.push(id);
1982
2708
 
1983
- // if too many active uploads, wait...
1984
- if (len <= this._options.maxConnections){
1985
- this._upload(id);
1986
- }
1987
- },
1988
- retry: function(id) {
1989
- var i = qq.indexOf(this._queue, id);
1990
- if (i >= 0) {
1991
- this._upload(id);
1992
- }
1993
- else {
1994
- this.upload(id);
1995
- }
1996
- },
1997
2709
  /**
1998
- * Cancels file upload by id
2710
+ * Removes element from queue, sends next request
1999
2711
  */
2000
- cancel: function(id){
2001
- this.log('Cancelling ' + id);
2002
- this._options.paramsStore.remove(id);
2003
- this._cancel(id);
2004
- this._dequeue(id);
2005
- },
2006
- /**
2007
- * Cancells all uploads
2008
- */
2009
- cancelAll: function(){
2010
- for (var i=0; i<this._queue.length; i++){
2011
- this._cancel(this._queue[i]);
2712
+ function dequeue(id) {
2713
+ var i = qq.indexOf(queue, id),
2714
+ max = options.maxConnections,
2715
+ nextId;
2716
+
2717
+ delete requestState[id];
2718
+ queue.splice(i, 1);
2719
+
2720
+ if (queue.length >= max && i < max){
2721
+ nextId = queue[max-1];
2722
+ sendRequest(nextId);
2012
2723
  }
2013
- this._queue = [];
2014
- },
2015
- /**
2016
- * Returns name of the file identified by id
2017
- */
2018
- getName: function(id){},
2019
- /**
2020
- * Returns size of the file identified by id
2021
- */
2022
- getSize: function(id){},
2023
- /**
2024
- * Returns id of files being uploaded or
2025
- * waiting for their turn
2026
- */
2027
- getQueue: function(){
2028
- return this._queue;
2029
- },
2030
- reset: function() {
2031
- this.log('Resetting upload handler');
2032
- this._queue = [];
2033
- },
2034
- /**
2035
- * Actual upload method
2036
- */
2037
- _upload: function(id){},
2038
- /**
2039
- * Actual cancel method
2040
- */
2041
- _cancel: function(id){},
2042
- /**
2043
- * Removes element from queue, starts upload of next
2044
- */
2045
- _dequeue: function(id){
2046
- var i = qq.indexOf(this._queue, id);
2047
- this._queue.splice(i, 1);
2724
+ }
2725
+
2726
+ function onComplete(id) {
2727
+ var xhr = requestState[id].xhr,
2728
+ method = getMethod(),
2729
+ isError = false;
2048
2730
 
2049
- var max = this._options.maxConnections;
2731
+ dequeue(id);
2050
2732
 
2051
- if (this._queue.length >= max && i < max){
2052
- var nextId = this._queue[max-1];
2053
- this._upload(nextId);
2733
+ if (!isResponseSuccessful(xhr.status)) {
2734
+ isError = true;
2735
+ log(method + " request for " + id + " has failed - response code " + xhr.status, "error");
2054
2736
  }
2055
- },
2056
- /**
2057
- * Determine if the file exists.
2058
- */
2059
- isValid: function(id) {}
2060
- };
2061
- /**
2062
- * Class for uploading files using form and iframe
2063
- * @inherits qq.UploadHandlerAbstract
2064
- */
2065
- qq.UploadHandlerForm = function(o){
2066
- qq.UploadHandlerAbstract.apply(this, arguments);
2067
2737
 
2068
- this._inputs = {};
2069
- this._detach_load_events = {};
2070
- };
2071
- // @inherits qq.UploadHandlerAbstract
2072
- qq.extend(qq.UploadHandlerForm.prototype, qq.UploadHandlerAbstract.prototype);
2738
+ options.onComplete(id, xhr, isError);
2739
+ }
2073
2740
 
2074
- qq.extend(qq.UploadHandlerForm.prototype, {
2075
- add: function(fileInput){
2076
- fileInput.setAttribute('name', this._options.inputName);
2077
- var id = qq.getUniqueId();
2741
+ function sendRequest(id) {
2742
+ var xhr = new XMLHttpRequest(),
2743
+ method = getMethod(),
2744
+ params = {},
2745
+ url;
2078
2746
 
2079
- this._inputs[id] = fileInput;
2747
+ options.onSend(id);
2080
2748
 
2081
- // remove file input from DOM
2082
- if (fileInput.parentNode){
2083
- qq(fileInput).remove();
2749
+ if (options.paramsStore.getParams) {
2750
+ params = options.paramsStore.getParams(id);
2084
2751
  }
2085
2752
 
2086
- return id;
2087
- },
2088
- getName: function(id){
2089
- // get input value and remove path to normalize
2090
- return this._inputs[id].value.replace(/.*(\/|\\)/, "");
2091
- },
2092
- isValid: function(id) {
2093
- return this._inputs[id] !== undefined;
2094
- },
2095
- reset: function() {
2096
- qq.UploadHandlerAbstract.prototype.reset.apply(this, arguments);
2097
- this._inputs = {};
2098
- this._detach_load_events = {};
2099
- },
2100
- _cancel: function(id){
2101
- this._options.onCancel(id, this.getName(id));
2753
+ url = createUrl(id, params);
2754
+
2755
+ requestState[id].xhr = xhr;
2756
+ xhr.onreadystatechange = getReadyStateChangeHandler(id);
2757
+ xhr.open(method, url, true);
2102
2758
 
2103
- delete this._inputs[id];
2104
- delete this._detach_load_events[id];
2759
+ if (options.cors.expected && options.cors.sendCredentials) {
2760
+ xhr.withCredentials = true;
2761
+ }
2105
2762
 
2106
- var iframe = document.getElementById(id);
2107
- if (iframe){
2108
- // to cancel request set src to something else
2109
- // we use src="javascript:false;" because it doesn't
2110
- // trigger ie6 prompt on https
2111
- iframe.setAttribute('src', 'javascript:false;');
2763
+ setHeaders(id);
2112
2764
 
2113
- qq(iframe).remove();
2765
+ log('Sending ' + method + " request for " + id);
2766
+ if (!shouldParamsBeInQueryString && params) {
2767
+ xhr.send(qq.obj2url(params, ""));
2114
2768
  }
2115
- },
2116
- _upload: function(id){
2117
- this._options.onUpload(id, this.getName(id), false);
2118
- var input = this._inputs[id];
2769
+ else {
2770
+ xhr.send();
2771
+ }
2772
+ }
2773
+
2774
+ function createUrl(id, params) {
2775
+ var endpoint = options.endpointStore.getEndpoint(id),
2776
+ addToPath = requestState[id].addToPath;
2119
2777
 
2120
- if (!input){
2121
- throw new Error('file with passed id was not added, or already uploaded or cancelled');
2778
+ if (addToPath !== undefined) {
2779
+ endpoint += "/" + addToPath;
2122
2780
  }
2123
2781
 
2124
- var fileName = this.getName(id);
2782
+ if (shouldParamsBeInQueryString && params) {
2783
+ return qq.obj2url(params, endpoint);
2784
+ }
2785
+ else {
2786
+ return endpoint;
2787
+ }
2788
+ }
2125
2789
 
2126
- var iframe = this._createIframe(id);
2127
- var form = this._createForm(iframe, this._options.paramsStore.getParams(id));
2128
- form.appendChild(input);
2790
+ function getReadyStateChangeHandler(id) {
2791
+ var xhr = requestState[id].xhr;
2129
2792
 
2130
- var self = this;
2131
- this._attachLoadEvent(iframe, function(){
2132
- self.log('iframe loaded');
2793
+ return function() {
2794
+ if (xhr.readyState === 4) {
2795
+ onComplete(id, xhr);
2796
+ }
2797
+ };
2798
+ }
2133
2799
 
2134
- var response = self._getIframeContentJSON(iframe);
2800
+ function setHeaders(id) {
2801
+ var xhr = requestState[id].xhr,
2802
+ customHeaders = options.customHeaders;
2135
2803
 
2136
- // timeout added to fix busy state in FF3.6
2137
- setTimeout(function(){
2138
- self._detach_load_events[id]();
2139
- delete self._detach_load_events[id];
2140
- qq(iframe).remove();
2141
- }, 1);
2804
+ xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
2805
+ xhr.setRequestHeader("Cache-Control", "no-cache");
2142
2806
 
2143
- if (!response.success) {
2144
- if (self._options.onAutoRetry(id, fileName, response)) {
2145
- return;
2146
- }
2147
- }
2148
- self._options.onComplete(id, fileName, response);
2149
- self._dequeue(id);
2807
+ qq.each(customHeaders, function(name, val) {
2808
+ xhr.setRequestHeader(name, val);
2150
2809
  });
2810
+ }
2151
2811
 
2152
- this.log('Sending upload request for ' + id);
2153
- form.submit();
2154
- qq(form).remove();
2812
+ function cancelRequest(id) {
2813
+ var xhr = requestState[id].xhr,
2814
+ method = getMethod();
2155
2815
 
2156
- return id;
2157
- },
2158
- _attachLoadEvent: function(iframe, callback){
2159
- var self = this;
2160
- this._detach_load_events[iframe.id] = qq(iframe).attach('load', function(){
2161
- self.log('Received response for ' + iframe.id);
2816
+ if (xhr) {
2817
+ xhr.onreadystatechange = null;
2818
+ xhr.abort();
2819
+ dequeue(id);
2162
2820
 
2163
- // when we remove iframe from dom
2164
- // the request stops, but in IE load
2165
- // event fires
2166
- if (!iframe.parentNode){
2167
- return;
2168
- }
2821
+ log('Cancelled ' + method + " for " + id);
2822
+ options.onCancel(id);
2169
2823
 
2170
- try {
2171
- // fixing Opera 10.53
2172
- if (iframe.contentDocument &&
2173
- iframe.contentDocument.body &&
2174
- iframe.contentDocument.body.innerHTML == "false"){
2175
- // In Opera event is fired second time
2176
- // when body.innerHTML changed from false
2177
- // to server response approx. after 1 sec
2178
- // when we upload file with iframe
2179
- return;
2180
- }
2181
- }
2182
- catch (error) {
2183
- //IE may throw an "access is denied" error when attempting to access contentDocument on the iframe in some cases
2184
- self.log('Error when attempting to access iframe during handling of upload response (' + error + ")", 'error');
2185
- }
2824
+ return true;
2825
+ }
2186
2826
 
2187
- callback();
2188
- });
2189
- },
2190
- /**
2191
- * Returns json object received by iframe from server.
2192
- */
2193
- _getIframeContentJSON: function(iframe){
2194
- //IE may throw an "access is denied" error when attempting to access contentDocument on the iframe in some cases
2195
- try {
2196
- // iframe.contentWindow.document - for IE<7
2197
- var doc = iframe.contentDocument ? iframe.contentDocument: iframe.contentWindow.document,
2198
- response;
2827
+ return false;
2828
+ }
2199
2829
 
2200
- var innerHTML = doc.body.innerHTML;
2201
- this.log("converting iframe's innerHTML to JSON");
2202
- this.log("innerHTML = " + innerHTML);
2203
- //plain text response may be wrapped in <pre> tag
2204
- if (innerHTML && innerHTML.match(/^<pre/i)) {
2205
- innerHTML = doc.body.firstChild.firstChild.nodeValue;
2206
- }
2207
- response = eval("(" + innerHTML + ")");
2208
- } catch(error){
2209
- this.log('Error when attempting to parse form upload response (' + error + ")", 'error');
2210
- response = {success: false};
2830
+ function isResponseSuccessful(responseCode) {
2831
+ return qq.indexOf(options.successfulResponseCodes, responseCode) >= 0;
2832
+ }
2833
+
2834
+ function getMethod() {
2835
+ if (options.demoMode) {
2836
+ return "GET";
2211
2837
  }
2212
2838
 
2213
- return response;
2214
- },
2215
- /**
2216
- * Creates iframe with unique name
2217
- */
2218
- _createIframe: function(id){
2219
- // We can't use following code as the name attribute
2220
- // won't be properly registered in IE6, and new window
2221
- // on form submit will open
2222
- // var iframe = document.createElement('iframe');
2223
- // iframe.setAttribute('name', id);
2839
+ return options.method;
2840
+ }
2224
2841
 
2225
- var iframe = qq.toElement('<iframe src="javascript:false;" name="' + id + '" />');
2226
- // src="javascript:false;" removes ie6 prompt on https
2227
2842
 
2228
- iframe.setAttribute('id', id);
2843
+ return {
2844
+ send: function(id, addToPath) {
2845
+ requestState[id] = {
2846
+ addToPath: addToPath
2847
+ };
2229
2848
 
2230
- iframe.style.display = 'none';
2849
+ var len = queue.push(id);
2850
+
2851
+ // if too many active connections, wait...
2852
+ if (len <= options.maxConnections){
2853
+ sendRequest(id);
2854
+ }
2855
+ },
2856
+ cancel: function(id) {
2857
+ return cancelRequest(id);
2858
+ }
2859
+ };
2860
+ };
2861
+ /** Generic class for sending non-upload ajax requests and handling the associated responses **/
2862
+ /*globals qq, XMLHttpRequest*/
2863
+ qq.DeleteFileAjaxRequestor = function(o) {
2864
+ "use strict";
2865
+
2866
+ var requestor,
2867
+ options = {
2868
+ endpointStore: {},
2869
+ maxConnections: 3,
2870
+ customHeaders: {},
2871
+ paramsStore: {},
2872
+ demoMode: false,
2873
+ cors: {
2874
+ expected: false,
2875
+ sendCredentials: false
2876
+ },
2877
+ log: function(str, level) {},
2878
+ onDelete: function(id) {},
2879
+ onDeleteComplete: function(id, xhr, isError) {}
2880
+ };
2881
+
2882
+ qq.extend(options, o);
2883
+
2884
+ requestor = new qq.AjaxRequestor({
2885
+ method: 'DELETE',
2886
+ endpointStore: options.endpointStore,
2887
+ paramsStore: options.paramsStore,
2888
+ maxConnections: options.maxConnections,
2889
+ customHeaders: options.customHeaders,
2890
+ successfulResponseCodes: [200, 202, 204],
2891
+ demoMode: options.demoMode,
2892
+ log: options.log,
2893
+ onSend: options.onDelete,
2894
+ onComplete: options.onDeleteComplete
2895
+ });
2896
+
2897
+
2898
+ return {
2899
+ sendDelete: function(id, uuid) {
2900
+ requestor.send(id, uuid);
2901
+ options.log("Submitted delete file request for " + id);
2902
+ }
2903
+ };
2904
+ };
2905
+ qq.WindowReceiveMessage = function(o) {
2906
+ var options = {
2907
+ log: function(message, level) {}
2908
+ },
2909
+ callbackWrapperDetachers = {};
2910
+
2911
+ qq.extend(options, o);
2912
+
2913
+ return {
2914
+ receiveMessage : function(id, callback) {
2915
+ var onMessageCallbackWrapper = function(event) {
2916
+ callback(event.data);
2917
+ };
2918
+
2919
+ if (window.postMessage) {
2920
+ callbackWrapperDetachers[id] = qq(window).attach("message", onMessageCallbackWrapper);
2921
+ }
2922
+ else {
2923
+ log("iframe message passing not supported in this browser!", "error");
2924
+ }
2925
+ },
2926
+
2927
+ stopReceivingMessages : function(id) {
2928
+ if (window.postMessage) {
2929
+ var detacher = callbackWrapperDetachers[id];
2930
+ if (detacher) {
2931
+ detacher();
2932
+ }
2933
+ }
2934
+ }
2935
+ };
2936
+ };
2937
+ /**
2938
+ * Class for uploading files, uploading itself is handled by child classes
2939
+ */
2940
+ /*globals qq*/
2941
+ qq.UploadHandler = function(o) {
2942
+ "use strict";
2943
+
2944
+ var queue = [],
2945
+ options, log, dequeue, handlerImpl;
2946
+
2947
+ // Default options, can be overridden by the user
2948
+ options = {
2949
+ debug: false,
2950
+ forceMultipart: true,
2951
+ paramsInBody: false,
2952
+ paramsStore: {},
2953
+ endpointStore: {},
2954
+ cors: {
2955
+ expected: false,
2956
+ sendCredentials: false
2957
+ },
2958
+ maxConnections: 3, // maximum number of concurrent uploads
2959
+ uuidParamName: 'qquuid',
2960
+ totalFileSizeParamName: 'qqtotalfilesize',
2961
+ chunking: {
2962
+ enabled: false,
2963
+ partSize: 2000000, //bytes
2964
+ paramNames: {
2965
+ partIndex: 'qqpartindex',
2966
+ partByteOffset: 'qqpartbyteoffset',
2967
+ chunkSize: 'qqchunksize',
2968
+ totalParts: 'qqtotalparts',
2969
+ filename: 'qqfilename'
2970
+ }
2971
+ },
2972
+ resume: {
2973
+ enabled: false,
2974
+ id: null,
2975
+ cookiesExpireIn: 7, //days
2976
+ paramNames: {
2977
+ resuming: "qqresume"
2978
+ }
2979
+ },
2980
+ blobs: {
2981
+ paramNames: {
2982
+ name: 'qqblobname'
2983
+ }
2984
+ },
2985
+ log: function(str, level) {},
2986
+ onProgress: function(id, fileName, loaded, total){},
2987
+ onComplete: function(id, fileName, response, xhr){},
2988
+ onCancel: function(id, fileName){},
2989
+ onUpload: function(id, fileName){},
2990
+ onUploadChunk: function(id, fileName, chunkData){},
2991
+ onAutoRetry: function(id, fileName, response, xhr){},
2992
+ onResume: function(id, fileName, chunkData){}
2993
+
2994
+ };
2995
+ qq.extend(options, o);
2996
+
2997
+ log = options.log;
2998
+
2999
+ /**
3000
+ * Removes element from queue, starts upload of next
3001
+ */
3002
+ dequeue = function(id) {
3003
+ var i = qq.indexOf(queue, id),
3004
+ max = options.maxConnections,
3005
+ nextId;
3006
+
3007
+ if (i >= 0) {
3008
+ queue.splice(i, 1);
3009
+
3010
+ if (queue.length >= max && i < max){
3011
+ nextId = queue[max-1];
3012
+ handlerImpl.upload(nextId);
3013
+ }
3014
+ }
3015
+ };
3016
+
3017
+ if (qq.isXhrUploadSupported()) {
3018
+ handlerImpl = new qq.UploadHandlerXhr(options, dequeue, log);
3019
+ }
3020
+ else {
3021
+ handlerImpl = new qq.UploadHandlerForm(options, dequeue, log);
3022
+ }
3023
+
3024
+
3025
+ return {
3026
+ /**
3027
+ * Adds file or file input to the queue
3028
+ * @returns id
3029
+ **/
3030
+ add: function(file){
3031
+ return handlerImpl.add(file);
3032
+ },
3033
+ /**
3034
+ * Sends the file identified by id
3035
+ */
3036
+ upload: function(id){
3037
+ var len = queue.push(id);
3038
+
3039
+ // if too many active uploads, wait...
3040
+ if (len <= options.maxConnections){
3041
+ return handlerImpl.upload(id);
3042
+ }
3043
+ },
3044
+ retry: function(id) {
3045
+ var i = qq.indexOf(queue, id);
3046
+ if (i >= 0) {
3047
+ return handlerImpl.upload(id, true);
3048
+ }
3049
+ else {
3050
+ return this.upload(id);
3051
+ }
3052
+ },
3053
+ /**
3054
+ * Cancels file upload by id
3055
+ */
3056
+ cancel: function(id) {
3057
+ log('Cancelling ' + id);
3058
+ options.paramsStore.remove(id);
3059
+ handlerImpl.cancel(id);
3060
+ dequeue(id);
3061
+ },
3062
+ /**
3063
+ * Cancels all queued or in-progress uploads
3064
+ */
3065
+ cancelAll: function() {
3066
+ var self = this,
3067
+ queueCopy = [];
3068
+
3069
+ qq.extend(queueCopy, queue);
3070
+ qq.each(queueCopy, function(idx, fileId) {
3071
+ self.cancel(fileId);
3072
+ });
3073
+
3074
+ queue = [];
3075
+ },
3076
+ /**
3077
+ * Returns name of the file identified by id
3078
+ */
3079
+ getName: function(id){
3080
+ return handlerImpl.getName(id);
3081
+ },
3082
+ /**
3083
+ * Returns size of the file identified by id
3084
+ */
3085
+ getSize: function(id){
3086
+ if (handlerImpl.getSize) {
3087
+ return handlerImpl.getSize(id);
3088
+ }
3089
+ },
3090
+ getFile: function(id) {
3091
+ if (handlerImpl.getFile) {
3092
+ return handlerImpl.getFile(id);
3093
+ }
3094
+ },
3095
+ /**
3096
+ * Returns id of files being uploaded or
3097
+ * waiting for their turn
3098
+ */
3099
+ getQueue: function(){
3100
+ return queue;
3101
+ },
3102
+ reset: function() {
3103
+ log('Resetting upload handler');
3104
+ queue = [];
3105
+ handlerImpl.reset();
3106
+ },
3107
+ getUuid: function(id) {
3108
+ return handlerImpl.getUuid(id);
3109
+ },
3110
+ /**
3111
+ * Determine if the file exists.
3112
+ */
3113
+ isValid: function(id) {
3114
+ return handlerImpl.isValid(id);
3115
+ },
3116
+ getResumableFilesData: function() {
3117
+ if (handlerImpl.getResumableFilesData) {
3118
+ return handlerImpl.getResumableFilesData();
3119
+ }
3120
+ return [];
3121
+ }
3122
+ };
3123
+ };
3124
+ /*globals qq, document, setTimeout*/
3125
+ /*globals clearTimeout*/
3126
+ qq.UploadHandlerForm = function(o, uploadCompleteCallback, logCallback) {
3127
+ "use strict";
3128
+
3129
+ var options = o,
3130
+ inputs = [],
3131
+ uuids = [],
3132
+ detachLoadEvents = {},
3133
+ postMessageCallbackTimers = {},
3134
+ uploadComplete = uploadCompleteCallback,
3135
+ log = logCallback,
3136
+ corsMessageReceiver = new qq.WindowReceiveMessage({log: log}),
3137
+ onloadCallbacks = {},
3138
+ api;
3139
+
3140
+
3141
+ function detachLoadEvent(id) {
3142
+ if (detachLoadEvents[id] !== undefined) {
3143
+ detachLoadEvents[id]();
3144
+ delete detachLoadEvents[id];
3145
+ }
3146
+ }
3147
+
3148
+ function registerPostMessageCallback(iframe, callback) {
3149
+ var id = iframe.id;
3150
+
3151
+ onloadCallbacks[uuids[id]] = callback;
3152
+
3153
+ detachLoadEvents[id] = qq(iframe).attach('load', function() {
3154
+ if (inputs[id]) {
3155
+ log("Received iframe load event for CORS upload request (file id " + id + ")");
3156
+
3157
+ postMessageCallbackTimers[id] = setTimeout(function() {
3158
+ var errorMessage = "No valid message received from loaded iframe for file id " + id;
3159
+ log(errorMessage, "error");
3160
+ callback({
3161
+ error: errorMessage
3162
+ });
3163
+ }, 1000);
3164
+ }
3165
+ });
3166
+
3167
+ corsMessageReceiver.receiveMessage(id, function(message) {
3168
+ log("Received the following window message: '" + message + "'");
3169
+ var response = qq.parseJson(message),
3170
+ uuid = response.uuid,
3171
+ onloadCallback;
3172
+
3173
+ if (uuid && onloadCallbacks[uuid]) {
3174
+ clearTimeout(postMessageCallbackTimers[id]);
3175
+ delete postMessageCallbackTimers[id];
3176
+
3177
+ detachLoadEvent(id);
3178
+
3179
+ onloadCallback = onloadCallbacks[uuid];
3180
+
3181
+ delete onloadCallbacks[uuid];
3182
+ corsMessageReceiver.stopReceivingMessages(id);
3183
+ onloadCallback(response);
3184
+ }
3185
+ else if (!uuid) {
3186
+ log("'" + message + "' does not contain a UUID - ignoring.");
3187
+ }
3188
+ });
3189
+ }
3190
+
3191
+ function attachLoadEvent(iframe, callback) {
3192
+ /*jslint eqeq: true*/
3193
+
3194
+ if (options.cors.expected) {
3195
+ registerPostMessageCallback(iframe, callback);
3196
+ }
3197
+ else {
3198
+ detachLoadEvents[iframe.id] = qq(iframe).attach('load', function(){
3199
+ log('Received response for ' + iframe.id);
3200
+
3201
+ // when we remove iframe from dom
3202
+ // the request stops, but in IE load
3203
+ // event fires
3204
+ if (!iframe.parentNode){
3205
+ return;
3206
+ }
3207
+
3208
+ try {
3209
+ // fixing Opera 10.53
3210
+ if (iframe.contentDocument &&
3211
+ iframe.contentDocument.body &&
3212
+ iframe.contentDocument.body.innerHTML == "false"){
3213
+ // In Opera event is fired second time
3214
+ // when body.innerHTML changed from false
3215
+ // to server response approx. after 1 sec
3216
+ // when we upload file with iframe
3217
+ return;
3218
+ }
3219
+ }
3220
+ catch (error) {
3221
+ //IE may throw an "access is denied" error when attempting to access contentDocument on the iframe in some cases
3222
+ log('Error when attempting to access iframe during handling of upload response (' + error + ")", 'error');
3223
+ }
3224
+
3225
+ callback();
3226
+ });
3227
+ }
3228
+ }
3229
+
3230
+ /**
3231
+ * Returns json object received by iframe from server.
3232
+ */
3233
+ function getIframeContentJson(iframe) {
3234
+ /*jshint evil: true*/
3235
+
3236
+ var response;
3237
+
3238
+ //IE may throw an "access is denied" error when attempting to access contentDocument on the iframe in some cases
3239
+ try {
3240
+ // iframe.contentWindow.document - for IE<7
3241
+ var doc = iframe.contentDocument || iframe.contentWindow.document,
3242
+ innerHTML = doc.body.innerHTML;
3243
+
3244
+ log("converting iframe's innerHTML to JSON");
3245
+ log("innerHTML = " + innerHTML);
3246
+ //plain text response may be wrapped in <pre> tag
3247
+ if (innerHTML && innerHTML.match(/^<pre/i)) {
3248
+ innerHTML = doc.body.firstChild.firstChild.nodeValue;
3249
+ }
3250
+
3251
+ response = qq.parseJson(innerHTML);
3252
+ } catch(error){
3253
+ log('Error when attempting to parse form upload response (' + error + ")", 'error');
3254
+ response = {success: false};
3255
+ }
3256
+
3257
+ return response;
3258
+ }
3259
+
3260
+ /**
3261
+ * Creates iframe with unique name
3262
+ */
3263
+ function createIframe(id){
3264
+ // We can't use following code as the name attribute
3265
+ // won't be properly registered in IE6, and new window
3266
+ // on form submit will open
3267
+ // var iframe = document.createElement('iframe');
3268
+ // iframe.setAttribute('name', id);
3269
+
3270
+ var iframe = qq.toElement('<iframe src="javascript:false;" name="' + id + '" />');
3271
+
3272
+ iframe.setAttribute('id', id);
3273
+
3274
+ iframe.style.display = 'none';
2231
3275
  document.body.appendChild(iframe);
2232
3276
 
2233
3277
  return iframe;
2234
- },
3278
+ }
3279
+
2235
3280
  /**
2236
3281
  * Creates form, that will be submitted to iframe
2237
3282
  */
2238
- _createForm: function(iframe, params){
2239
- // We can't use the following code in IE6
2240
- // var form = document.createElement('form');
2241
- // form.setAttribute('method', 'post');
2242
- // form.setAttribute('enctype', 'multipart/form-data');
2243
- // Because in this case file won't be attached to request
2244
- var protocol = this._options.demoMode ? "GET" : "POST",
3283
+ function createForm(id, iframe){
3284
+ var params = options.paramsStore.getParams(id),
3285
+ protocol = options.demoMode ? "GET" : "POST",
2245
3286
  form = qq.toElement('<form method="' + protocol + '" enctype="multipart/form-data"></form>'),
2246
- url = this._options.endpoint;
3287
+ endpoint = options.endpointStore.getEndpoint(id),
3288
+ url = endpoint;
2247
3289
 
2248
- if (!this._options.paramsInBody) {
2249
- url = qq.obj2url(params, this._options.endpoint);
3290
+ params[options.uuidParamName] = uuids[id];
3291
+
3292
+ if (!options.paramsInBody) {
3293
+ url = qq.obj2url(params, endpoint);
2250
3294
  }
2251
3295
  else {
2252
3296
  qq.obj2Inputs(params, form);
@@ -2259,175 +3303,743 @@ qq.extend(qq.UploadHandlerForm.prototype, {
2259
3303
 
2260
3304
  return form;
2261
3305
  }
2262
- });
2263
- /**
2264
- * Class for uploading files using xhr
2265
- * @inherits qq.UploadHandlerAbstract
2266
- */
2267
- qq.UploadHandlerXhr = function(o){
2268
- qq.UploadHandlerAbstract.apply(this, arguments);
2269
3306
 
2270
- this._files = [];
2271
- this._xhrs = [];
2272
3307
 
2273
- // current loaded size in bytes for each file
2274
- this._loaded = [];
2275
- };
3308
+ api = {
3309
+ add: function(fileInput) {
3310
+ fileInput.setAttribute('name', options.inputName);
2276
3311
 
2277
- // @inherits qq.UploadHandlerAbstract
2278
- qq.extend(qq.UploadHandlerXhr.prototype, qq.UploadHandlerAbstract.prototype)
3312
+ var id = inputs.push(fileInput) - 1;
3313
+ uuids[id] = qq.getUniqueId();
2279
3314
 
2280
- qq.extend(qq.UploadHandlerXhr.prototype, {
2281
- /**
2282
- * Adds file to the queue
2283
- * Returns id to use with upload, cancel
2284
- **/
2285
- add: function(file){
2286
- if (!(file instanceof File)){
2287
- throw new Error('Passed obj in not a File (in qq.UploadHandlerXhr)');
2288
- }
2289
-
2290
- return this._files.push(file) - 1;
2291
- },
2292
- getName: function(id){
2293
- var file = this._files[id];
2294
- // fix missing name in Safari 4
2295
- //NOTE: fixed missing name firefox 11.0a2 file.fileName is actually undefined
2296
- return (file.fileName !== null && file.fileName !== undefined) ? file.fileName : file.name;
2297
- },
2298
- getSize: function(id){
2299
- var file = this._files[id];
2300
- return file.fileSize != null ? file.fileSize : file.size;
2301
- },
2302
- /**
2303
- * Returns uploaded bytes for file identified by id
2304
- */
2305
- getLoaded: function(id){
2306
- return this._loaded[id] || 0;
2307
- },
2308
- isValid: function(id) {
2309
- return this._files[id] !== undefined;
2310
- },
2311
- reset: function() {
2312
- qq.UploadHandlerAbstract.prototype.reset.apply(this, arguments);
2313
- this._files = [];
2314
- this._xhrs = [];
2315
- this._loaded = [];
2316
- },
2317
- /**
2318
- * Sends the file identified by id to the server
2319
- */
2320
- _upload: function(id){
2321
- var file = this._files[id],
2322
- name = this.getName(id),
2323
- size = this.getSize(id),
2324
- self = this,
2325
- url = this._options.endpoint,
2326
- protocol = this._options.demoMode ? "GET" : "POST",
2327
- xhr, formData, paramName, key, params;
3315
+ // remove file input from DOM
3316
+ if (fileInput.parentNode){
3317
+ qq(fileInput).remove();
3318
+ }
2328
3319
 
2329
- this._options.onUpload(id, this.getName(id), true);
3320
+ return id;
3321
+ },
3322
+ getName: function(id) {
3323
+ /*jslint regexp: true*/
2330
3324
 
2331
- this._loaded[id] = 0;
3325
+ if (api.isValid(id)) {
3326
+ // get input value and remove path to normalize
3327
+ return inputs[id].value.replace(/.*(\/|\\)/, "");
3328
+ }
3329
+ else {
3330
+ log(id + " is not a valid item ID.", "error");
3331
+ }
3332
+ },
3333
+ isValid: function(id) {
3334
+ return inputs[id] !== undefined;
3335
+ },
3336
+ reset: function() {
3337
+ inputs = [];
3338
+ uuids = [];
3339
+ detachLoadEvents = {};
3340
+ },
3341
+ getUuid: function(id) {
3342
+ return uuids[id];
3343
+ },
3344
+ cancel: function(id) {
3345
+ options.onCancel(id, this.getName(id));
2332
3346
 
2333
- xhr = this._xhrs[id] = new XMLHttpRequest();
3347
+ delete inputs[id];
3348
+ delete uuids[id];
3349
+ delete detachLoadEvents[id];
2334
3350
 
2335
- xhr.upload.onprogress = function(e){
2336
- if (e.lengthComputable){
2337
- self._loaded[id] = e.loaded;
2338
- self._options.onProgress(id, name, e.loaded, e.total);
3351
+ if (options.cors.expected) {
3352
+ clearTimeout(postMessageCallbackTimers[id]);
3353
+ delete postMessageCallbackTimers[id];
3354
+ corsMessageReceiver.stopReceivingMessages(id);
2339
3355
  }
2340
- };
2341
3356
 
2342
- xhr.onreadystatechange = function(){
2343
- if (xhr.readyState === 4){
2344
- self._onComplete(id, xhr);
3357
+ var iframe = document.getElementById(id);
3358
+ if (iframe) {
3359
+ // to cancel request set src to something else
3360
+ // we use src="javascript:false;" because it doesn't
3361
+ // trigger ie6 prompt on https
3362
+ iframe.setAttribute('src', 'java' + String.fromCharCode(115) + 'cript:false;'); //deal with "JSLint: javascript URL" warning, which apparently cannot be turned off
3363
+
3364
+ qq(iframe).remove();
2345
3365
  }
3366
+ },
3367
+ upload: function(id){
3368
+ var input = inputs[id],
3369
+ fileName = api.getName(id),
3370
+ iframe = createIframe(id),
3371
+ form;
3372
+
3373
+ if (!input){
3374
+ throw new Error('file with passed id was not added, or already uploaded or cancelled');
3375
+ }
3376
+
3377
+ options.onUpload(id, this.getName(id));
3378
+
3379
+ form = createForm(id, iframe);
3380
+ form.appendChild(input);
3381
+
3382
+ attachLoadEvent(iframe, function(responseFromMessage){
3383
+ log('iframe loaded');
3384
+
3385
+ var response = responseFromMessage ? responseFromMessage : getIframeContentJson(iframe);
3386
+
3387
+ detachLoadEvent(id);
3388
+
3389
+ //we can't remove an iframe if the iframe doesn't belong to the same domain
3390
+ if (!options.cors.expected) {
3391
+ qq(iframe).remove();
3392
+ }
3393
+
3394
+ if (!response.success) {
3395
+ if (options.onAutoRetry(id, fileName, response)) {
3396
+ return;
3397
+ }
3398
+ }
3399
+ options.onComplete(id, fileName, response);
3400
+ uploadComplete(id);
3401
+ });
3402
+
3403
+ log('Sending upload request for ' + id);
3404
+ form.submit();
3405
+ qq(form).remove();
3406
+
3407
+ return id;
3408
+ }
3409
+ };
3410
+
3411
+ return api;
3412
+ };
3413
+ /*globals qq, File, XMLHttpRequest, FormData, Blob*/
3414
+ qq.UploadHandlerXhr = function(o, uploadCompleteCallback, logCallback) {
3415
+ "use strict";
3416
+
3417
+ var options = o,
3418
+ uploadComplete = uploadCompleteCallback,
3419
+ log = logCallback,
3420
+ fileState = [],
3421
+ cookieItemDelimiter = "|",
3422
+ chunkFiles = options.chunking.enabled && qq.isFileChunkingSupported(),
3423
+ resumeEnabled = options.resume.enabled && chunkFiles && qq.areCookiesEnabled(),
3424
+ resumeId = getResumeId(),
3425
+ multipart = options.forceMultipart || options.paramsInBody,
3426
+ api;
3427
+
3428
+
3429
+ function addChunkingSpecificParams(id, params, chunkData) {
3430
+ var size = api.getSize(id),
3431
+ name = api.getName(id);
3432
+
3433
+ params[options.chunking.paramNames.partIndex] = chunkData.part;
3434
+ params[options.chunking.paramNames.partByteOffset] = chunkData.start;
3435
+ params[options.chunking.paramNames.chunkSize] = chunkData.size;
3436
+ params[options.chunking.paramNames.totalParts] = chunkData.count;
3437
+ params[options.totalFileSizeParamName] = size;
3438
+
3439
+ /**
3440
+ * When a Blob is sent in a multipart request, the filename value in the content-disposition header is either "blob"
3441
+ * or an empty string. So, we will need to include the actual file name as a param in this case.
3442
+ */
3443
+ if (multipart) {
3444
+ params[options.chunking.paramNames.filename] = name;
3445
+ }
3446
+ }
3447
+
3448
+ function addResumeSpecificParams(params) {
3449
+ params[options.resume.paramNames.resuming] = true;
3450
+ }
3451
+
3452
+ function getChunk(fileOrBlob, startByte, endByte) {
3453
+ if (fileOrBlob.slice) {
3454
+ return fileOrBlob.slice(startByte, endByte);
3455
+ }
3456
+ else if (fileOrBlob.mozSlice) {
3457
+ return fileOrBlob.mozSlice(startByte, endByte);
3458
+ }
3459
+ else if (fileOrBlob.webkitSlice) {
3460
+ return fileOrBlob.webkitSlice(startByte, endByte);
3461
+ }
3462
+ }
3463
+
3464
+ function getChunkData(id, chunkIndex) {
3465
+ var chunkSize = options.chunking.partSize,
3466
+ fileSize = api.getSize(id),
3467
+ fileOrBlob = fileState[id].file || fileState[id].blobData.blob,
3468
+ startBytes = chunkSize * chunkIndex,
3469
+ endBytes = startBytes+chunkSize >= fileSize ? fileSize : startBytes+chunkSize,
3470
+ totalChunks = getTotalChunks(id);
3471
+
3472
+ return {
3473
+ part: chunkIndex,
3474
+ start: startBytes,
3475
+ end: endBytes,
3476
+ count: totalChunks,
3477
+ blob: getChunk(fileOrBlob, startBytes, endBytes),
3478
+ size: endBytes - startBytes
2346
3479
  };
3480
+ }
3481
+
3482
+ function getTotalChunks(id) {
3483
+ var fileSize = api.getSize(id),
3484
+ chunkSize = options.chunking.partSize;
3485
+
3486
+ return Math.ceil(fileSize / chunkSize);
3487
+ }
2347
3488
 
2348
- params = this._options.paramsStore.getParams(id);
3489
+ function createXhr(id) {
3490
+ var xhr = new XMLHttpRequest();
3491
+
3492
+ fileState[id].xhr = xhr;
3493
+
3494
+ return xhr;
3495
+ }
3496
+
3497
+ function setParamsAndGetEntityToSend(params, xhr, fileOrBlob, id) {
3498
+ var formData = new FormData(),
3499
+ method = options.demoMode ? "GET" : "POST",
3500
+ endpoint = options.endpointStore.getEndpoint(id),
3501
+ url = endpoint,
3502
+ name = api.getName(id),
3503
+ size = api.getSize(id),
3504
+ blobData = fileState[id].blobData;
3505
+
3506
+ params[options.uuidParamName] = fileState[id].uuid;
3507
+
3508
+ if (multipart) {
3509
+ params[options.totalFileSizeParamName] = size;
3510
+
3511
+ if (blobData) {
3512
+ /**
3513
+ * When a Blob is sent in a multipart request, the filename value in the content-disposition header is either "blob"
3514
+ * or an empty string. So, we will need to include the actual file name as a param in this case.
3515
+ */
3516
+ params[options.blobs.paramNames.name] = blobData.name;
3517
+ }
3518
+ }
2349
3519
 
2350
3520
  //build query string
2351
- if (!this._options.paramsInBody) {
2352
- params[this._options.inputName] = name;
2353
- url = qq.obj2url(params, this._options.endpoint);
3521
+ if (!options.paramsInBody) {
3522
+ if (!multipart) {
3523
+ params[options.inputName] = name;
3524
+ }
3525
+ url = qq.obj2url(params, endpoint);
2354
3526
  }
2355
3527
 
2356
- xhr.open(protocol, url, true);
2357
- xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
2358
- xhr.setRequestHeader("X-File-Name", encodeURIComponent(name));
2359
- xhr.setRequestHeader("Cache-Control", "no-cache");
2360
- if (this._options.forceMultipart || this._options.paramsInBody) {
2361
- formData = new FormData();
3528
+ xhr.open(method, url, true);
3529
+
3530
+ if (options.cors.expected && options.cors.sendCredentials) {
3531
+ xhr.withCredentials = true;
3532
+ }
2362
3533
 
2363
- if (this._options.paramsInBody) {
3534
+ if (multipart) {
3535
+ if (options.paramsInBody) {
2364
3536
  qq.obj2FormData(params, formData);
2365
3537
  }
2366
3538
 
2367
- formData.append(this._options.inputName, file);
2368
- file = formData;
2369
- } else {
3539
+ formData.append(options.inputName, fileOrBlob);
3540
+ return formData;
3541
+ }
3542
+
3543
+ return fileOrBlob;
3544
+ }
3545
+
3546
+ function setHeaders(id, xhr) {
3547
+ var extraHeaders = options.customHeaders,
3548
+ fileOrBlob = fileState[id].file || fileState[id].blobData.blob;
3549
+
3550
+ xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
3551
+ xhr.setRequestHeader("Cache-Control", "no-cache");
3552
+
3553
+ if (!multipart) {
2370
3554
  xhr.setRequestHeader("Content-Type", "application/octet-stream");
2371
3555
  //NOTE: return mime type in xhr works on chrome 16.0.9 firefox 11.0a2
2372
- xhr.setRequestHeader("X-Mime-Type", file.type);
3556
+ xhr.setRequestHeader("X-Mime-Type", fileOrBlob.type);
3557
+ }
3558
+
3559
+ qq.each(extraHeaders, function(name, val) {
3560
+ xhr.setRequestHeader(name, val);
3561
+ });
3562
+ }
3563
+
3564
+ function handleCompletedItem(id, response, xhr) {
3565
+ var name = api.getName(id),
3566
+ size = api.getSize(id);
3567
+
3568
+ fileState[id].attemptingResume = false;
3569
+
3570
+ options.onProgress(id, name, size, size);
3571
+
3572
+ options.onComplete(id, name, response, xhr);
3573
+ delete fileState[id].xhr;
3574
+ uploadComplete(id);
3575
+ }
3576
+
3577
+ function uploadNextChunk(id) {
3578
+ var chunkIdx = fileState[id].remainingChunkIdxs[0],
3579
+ chunkData = getChunkData(id, chunkIdx),
3580
+ xhr = createXhr(id),
3581
+ size = api.getSize(id),
3582
+ name = api.getName(id),
3583
+ toSend, params;
3584
+
3585
+ if (fileState[id].loaded === undefined) {
3586
+ fileState[id].loaded = 0;
3587
+ }
3588
+
3589
+ if (resumeEnabled && fileState[id].file) {
3590
+ persistChunkData(id, chunkData);
2373
3591
  }
2374
3592
 
2375
- for (key in this._options.customHeaders){
2376
- if (this._options.customHeaders.hasOwnProperty(key)) {
2377
- xhr.setRequestHeader(key, this._options.customHeaders[key]);
3593
+ xhr.onreadystatechange = getReadyStateChangeHandler(id, xhr);
3594
+
3595
+ xhr.upload.onprogress = function(e) {
3596
+ if (e.lengthComputable) {
3597
+ var totalLoaded = e.loaded + fileState[id].loaded,
3598
+ estTotalRequestsSize = calcAllRequestsSizeForChunkedUpload(id, chunkIdx, e.total);
3599
+
3600
+ options.onProgress(id, name, totalLoaded, estTotalRequestsSize);
2378
3601
  }
3602
+ };
3603
+
3604
+ options.onUploadChunk(id, name, getChunkDataForCallback(chunkData));
3605
+
3606
+ params = options.paramsStore.getParams(id);
3607
+ addChunkingSpecificParams(id, params, chunkData);
3608
+
3609
+ if (fileState[id].attemptingResume) {
3610
+ addResumeSpecificParams(params);
2379
3611
  }
2380
3612
 
2381
- this.log('Sending upload request for ' + id);
2382
- xhr.send(file);
2383
- },
2384
- _onComplete: function(id, xhr){
2385
- "use strict";
2386
- // the request was aborted/cancelled
2387
- if (!this._files[id]) { return; }
3613
+ toSend = setParamsAndGetEntityToSend(params, xhr, chunkData.blob, id);
3614
+ setHeaders(id, xhr);
2388
3615
 
2389
- var name = this.getName(id);
2390
- var size = this.getSize(id);
2391
- var response; //the parsed JSON response from the server, or the empty object if parsing failed.
3616
+ log('Sending chunked upload request for item ' + id + ": bytes " + (chunkData.start+1) + "-" + chunkData.end + " of " + size);
3617
+ xhr.send(toSend);
3618
+ }
2392
3619
 
2393
- this._options.onProgress(id, name, size, size);
3620
+ function calcAllRequestsSizeForChunkedUpload(id, chunkIdx, requestSize) {
3621
+ var chunkData = getChunkData(id, chunkIdx),
3622
+ blobSize = chunkData.size,
3623
+ overhead = requestSize - blobSize,
3624
+ size = api.getSize(id),
3625
+ chunkCount = chunkData.count,
3626
+ initialRequestOverhead = fileState[id].initialRequestOverhead,
3627
+ overheadDiff = overhead - initialRequestOverhead;
3628
+
3629
+ fileState[id].lastRequestOverhead = overhead;
3630
+
3631
+ if (chunkIdx === 0) {
3632
+ fileState[id].lastChunkIdxProgress = 0;
3633
+ fileState[id].initialRequestOverhead = overhead;
3634
+ fileState[id].estTotalRequestsSize = size + (chunkCount * overhead);
3635
+ }
3636
+ else if (fileState[id].lastChunkIdxProgress !== chunkIdx) {
3637
+ fileState[id].lastChunkIdxProgress = chunkIdx;
3638
+ fileState[id].estTotalRequestsSize += overheadDiff;
3639
+ }
2394
3640
 
2395
- this.log("xhr - server response received for " + id);
2396
- this.log("responseText = " + xhr.responseText);
3641
+ return fileState[id].estTotalRequestsSize;
3642
+ }
2397
3643
 
2398
- try {
2399
- if (typeof JSON.parse === "function") {
2400
- response = JSON.parse(xhr.responseText);
2401
- } else {
2402
- response = eval("(" + xhr.responseText + ")");
3644
+ function getLastRequestOverhead(id) {
3645
+ if (multipart) {
3646
+ return fileState[id].lastRequestOverhead;
3647
+ }
3648
+ else {
3649
+ return 0;
3650
+ }
3651
+ }
3652
+
3653
+ function handleSuccessfullyCompletedChunk(id, response, xhr) {
3654
+ var chunkIdx = fileState[id].remainingChunkIdxs.shift(),
3655
+ chunkData = getChunkData(id, chunkIdx);
3656
+
3657
+ fileState[id].attemptingResume = false;
3658
+ fileState[id].loaded += chunkData.size + getLastRequestOverhead(id);
3659
+
3660
+ if (fileState[id].remainingChunkIdxs.length > 0) {
3661
+ uploadNextChunk(id);
3662
+ }
3663
+ else {
3664
+ if (resumeEnabled) {
3665
+ deletePersistedChunkData(id);
2403
3666
  }
2404
- } catch(error){
2405
- this.log('Error when attempting to parse xhr response text (' + error + ')', 'error');
3667
+
3668
+ handleCompletedItem(id, response, xhr);
3669
+ }
3670
+ }
3671
+
3672
+ function isErrorResponse(xhr, response) {
3673
+ return xhr.status !== 200 || !response.success || response.reset;
3674
+ }
3675
+
3676
+ function parseResponse(xhr) {
3677
+ var response;
3678
+
3679
+ try {
3680
+ response = qq.parseJson(xhr.responseText);
3681
+ }
3682
+ catch(error) {
3683
+ log('Error when attempting to parse xhr response text (' + error + ')', 'error');
2406
3684
  response = {};
2407
3685
  }
2408
3686
 
2409
- if (xhr.status !== 200 || !response.success){
2410
- if (this._options.onAutoRetry(id, name, response, xhr)) {
2411
- return;
3687
+ return response;
3688
+ }
3689
+
3690
+ function handleResetResponse(id) {
3691
+ log('Server has ordered chunking effort to be restarted on next attempt for item ID ' + id, 'error');
3692
+
3693
+ if (resumeEnabled) {
3694
+ deletePersistedChunkData(id);
3695
+ fileState[id].attemptingResume = false;
3696
+ }
3697
+
3698
+ fileState[id].remainingChunkIdxs = [];
3699
+ delete fileState[id].loaded;
3700
+ delete fileState[id].estTotalRequestsSize;
3701
+ delete fileState[id].initialRequestOverhead;
3702
+ }
3703
+
3704
+ function handleResetResponseOnResumeAttempt(id) {
3705
+ fileState[id].attemptingResume = false;
3706
+ log("Server has declared that it cannot handle resume for item ID " + id + " - starting from the first chunk", 'error');
3707
+ handleResetResponse(id);
3708
+ api.upload(id, true);
3709
+ }
3710
+
3711
+ function handleNonResetErrorResponse(id, response, xhr) {
3712
+ var name = api.getName(id);
3713
+
3714
+ if (options.onAutoRetry(id, name, response, xhr)) {
3715
+ return;
3716
+ }
3717
+ else {
3718
+ handleCompletedItem(id, response, xhr);
3719
+ }
3720
+ }
3721
+
3722
+ function onComplete(id, xhr) {
3723
+ var response;
3724
+
3725
+ // the request was aborted/cancelled
3726
+ if (!fileState[id]) {
3727
+ return;
3728
+ }
3729
+
3730
+ log("xhr - server response received for " + id);
3731
+ log("responseText = " + xhr.responseText);
3732
+ response = parseResponse(xhr);
3733
+
3734
+ if (isErrorResponse(xhr, response)) {
3735
+ if (response.reset) {
3736
+ handleResetResponse(id);
3737
+ }
3738
+
3739
+ if (fileState[id].attemptingResume && response.reset) {
3740
+ handleResetResponseOnResumeAttempt(id);
2412
3741
  }
3742
+ else {
3743
+ handleNonResetErrorResponse(id, response, xhr);
3744
+ }
3745
+ }
3746
+ else if (chunkFiles) {
3747
+ handleSuccessfullyCompletedChunk(id, response, xhr);
3748
+ }
3749
+ else {
3750
+ handleCompletedItem(id, response, xhr);
2413
3751
  }
3752
+ }
3753
+
3754
+ function getChunkDataForCallback(chunkData) {
3755
+ return {
3756
+ partIndex: chunkData.part,
3757
+ startByte: chunkData.start + 1,
3758
+ endByte: chunkData.end,
3759
+ totalParts: chunkData.count
3760
+ };
3761
+ }
2414
3762
 
2415
- this._options.onComplete(id, name, response, xhr);
3763
+ function getReadyStateChangeHandler(id, xhr) {
3764
+ return function() {
3765
+ if (xhr.readyState === 4) {
3766
+ onComplete(id, xhr);
3767
+ }
3768
+ };
3769
+ }
2416
3770
 
2417
- this._xhrs[id] = null;
2418
- this._dequeue(id);
2419
- },
2420
- _cancel: function(id){
2421
- this._options.onCancel(id, this.getName(id));
3771
+ function persistChunkData(id, chunkData) {
3772
+ var fileUuid = api.getUuid(id),
3773
+ lastByteSent = fileState[id].loaded,
3774
+ initialRequestOverhead = fileState[id].initialRequestOverhead,
3775
+ estTotalRequestsSize = fileState[id].estTotalRequestsSize,
3776
+ cookieName = getChunkDataCookieName(id),
3777
+ cookieValue = fileUuid +
3778
+ cookieItemDelimiter + chunkData.part +
3779
+ cookieItemDelimiter + lastByteSent +
3780
+ cookieItemDelimiter + initialRequestOverhead +
3781
+ cookieItemDelimiter + estTotalRequestsSize,
3782
+ cookieExpDays = options.resume.cookiesExpireIn;
3783
+
3784
+ qq.setCookie(cookieName, cookieValue, cookieExpDays);
3785
+ }
2422
3786
 
2423
- this._files[id] = null;
3787
+ function deletePersistedChunkData(id) {
3788
+ if (fileState[id].file) {
3789
+ var cookieName = getChunkDataCookieName(id);
3790
+ qq.deleteCookie(cookieName);
3791
+ }
3792
+ }
2424
3793
 
2425
- if (this._xhrs[id]){
2426
- this._xhrs[id].abort();
2427
- this._xhrs[id] = null;
3794
+ function getPersistedChunkData(id) {
3795
+ var chunkCookieValue = qq.getCookie(getChunkDataCookieName(id)),
3796
+ filename = api.getName(id),
3797
+ sections, uuid, partIndex, lastByteSent, initialRequestOverhead, estTotalRequestsSize;
3798
+
3799
+ if (chunkCookieValue) {
3800
+ sections = chunkCookieValue.split(cookieItemDelimiter);
3801
+
3802
+ if (sections.length === 5) {
3803
+ uuid = sections[0];
3804
+ partIndex = parseInt(sections[1], 10);
3805
+ lastByteSent = parseInt(sections[2], 10);
3806
+ initialRequestOverhead = parseInt(sections[3], 10);
3807
+ estTotalRequestsSize = parseInt(sections[4], 10);
3808
+
3809
+ return {
3810
+ uuid: uuid,
3811
+ part: partIndex,
3812
+ lastByteSent: lastByteSent,
3813
+ initialRequestOverhead: initialRequestOverhead,
3814
+ estTotalRequestsSize: estTotalRequestsSize
3815
+ };
3816
+ }
3817
+ else {
3818
+ log('Ignoring previously stored resume/chunk cookie for ' + filename + " - old cookie format", "warn");
3819
+ }
2428
3820
  }
2429
3821
  }
2430
- });
3822
+
3823
+ function getChunkDataCookieName(id) {
3824
+ var filename = api.getName(id),
3825
+ fileSize = api.getSize(id),
3826
+ maxChunkSize = options.chunking.partSize,
3827
+ cookieName;
3828
+
3829
+ cookieName = "qqfilechunk" + cookieItemDelimiter + encodeURIComponent(filename) + cookieItemDelimiter + fileSize + cookieItemDelimiter + maxChunkSize;
3830
+
3831
+ if (resumeId !== undefined) {
3832
+ cookieName += cookieItemDelimiter + resumeId;
3833
+ }
3834
+
3835
+ return cookieName;
3836
+ }
3837
+
3838
+ function getResumeId() {
3839
+ if (options.resume.id !== null &&
3840
+ options.resume.id !== undefined &&
3841
+ !qq.isFunction(options.resume.id) &&
3842
+ !qq.isObject(options.resume.id)) {
3843
+
3844
+ return options.resume.id;
3845
+ }
3846
+ }
3847
+
3848
+ function handleFileChunkingUpload(id, retry) {
3849
+ var name = api.getName(id),
3850
+ firstChunkIndex = 0,
3851
+ persistedChunkInfoForResume, firstChunkDataForResume, currentChunkIndex;
3852
+
3853
+ if (!fileState[id].remainingChunkIdxs || fileState[id].remainingChunkIdxs.length === 0) {
3854
+ fileState[id].remainingChunkIdxs = [];
3855
+
3856
+ if (resumeEnabled && !retry && fileState[id].file) {
3857
+ persistedChunkInfoForResume = getPersistedChunkData(id);
3858
+ if (persistedChunkInfoForResume) {
3859
+ firstChunkDataForResume = getChunkData(id, persistedChunkInfoForResume.part);
3860
+ if (options.onResume(id, name, getChunkDataForCallback(firstChunkDataForResume)) !== false) {
3861
+ firstChunkIndex = persistedChunkInfoForResume.part;
3862
+ fileState[id].uuid = persistedChunkInfoForResume.uuid;
3863
+ fileState[id].loaded = persistedChunkInfoForResume.lastByteSent;
3864
+ fileState[id].estTotalRequestsSize = persistedChunkInfoForResume.estTotalRequestsSize;
3865
+ fileState[id].initialRequestOverhead = persistedChunkInfoForResume.initialRequestOverhead;
3866
+ fileState[id].attemptingResume = true;
3867
+ log('Resuming ' + name + " at partition index " + firstChunkIndex);
3868
+ }
3869
+ }
3870
+ }
3871
+
3872
+ for (currentChunkIndex = getTotalChunks(id)-1; currentChunkIndex >= firstChunkIndex; currentChunkIndex-=1) {
3873
+ fileState[id].remainingChunkIdxs.unshift(currentChunkIndex);
3874
+ }
3875
+ }
3876
+
3877
+ uploadNextChunk(id);
3878
+ }
3879
+
3880
+ function handleStandardFileUpload(id) {
3881
+ var fileOrBlob = fileState[id].file || fileState[id].blobData.blob,
3882
+ name = api.getName(id),
3883
+ xhr, params, toSend;
3884
+
3885
+ fileState[id].loaded = 0;
3886
+
3887
+ xhr = createXhr(id);
3888
+
3889
+ xhr.upload.onprogress = function(e){
3890
+ if (e.lengthComputable){
3891
+ fileState[id].loaded = e.loaded;
3892
+ options.onProgress(id, name, e.loaded, e.total);
3893
+ }
3894
+ };
3895
+
3896
+ xhr.onreadystatechange = getReadyStateChangeHandler(id, xhr);
3897
+
3898
+ params = options.paramsStore.getParams(id);
3899
+ toSend = setParamsAndGetEntityToSend(params, xhr, fileOrBlob, id);
3900
+ setHeaders(id, xhr);
3901
+
3902
+ log('Sending upload request for ' + id);
3903
+ xhr.send(toSend);
3904
+ }
3905
+
3906
+
3907
+ api = {
3908
+ /**
3909
+ * Adds File or Blob to the queue
3910
+ * Returns id to use with upload, cancel
3911
+ **/
3912
+ add: function(fileOrBlobData){
3913
+ var id;
3914
+
3915
+ if (fileOrBlobData instanceof File) {
3916
+ id = fileState.push({file: fileOrBlobData}) - 1;
3917
+ }
3918
+ else if (fileOrBlobData.blob instanceof Blob) {
3919
+ id = fileState.push({blobData: fileOrBlobData}) - 1;
3920
+ }
3921
+ else {
3922
+ throw new Error('Passed obj in not a File or BlobData (in qq.UploadHandlerXhr)');
3923
+ }
3924
+
3925
+ fileState[id].uuid = qq.getUniqueId();
3926
+ return id;
3927
+ },
3928
+ getName: function(id){
3929
+ if (api.isValid(id)) {
3930
+ var file = fileState[id].file,
3931
+ blobData = fileState[id].blobData;
3932
+
3933
+ if (file) {
3934
+ // fix missing name in Safari 4
3935
+ //NOTE: fixed missing name firefox 11.0a2 file.fileName is actually undefined
3936
+ return (file.fileName !== null && file.fileName !== undefined) ? file.fileName : file.name;
3937
+ }
3938
+ else {
3939
+ return blobData.name;
3940
+ }
3941
+ }
3942
+ else {
3943
+ log(id + " is not a valid item ID.", "error");
3944
+ }
3945
+ },
3946
+ getSize: function(id){
3947
+ /*jshint eqnull: true*/
3948
+ var fileOrBlob = fileState[id].file || fileState[id].blobData.blob;
3949
+
3950
+ if (qq.isFileOrInput(fileOrBlob)) {
3951
+ return fileOrBlob.fileSize != null ? fileOrBlob.fileSize : fileOrBlob.size;
3952
+ }
3953
+ else {
3954
+ return fileOrBlob.size;
3955
+ }
3956
+ },
3957
+ getFile: function(id) {
3958
+ if (fileState[id]) {
3959
+ return fileState[id].file || fileState[id].blobData.blob;
3960
+ }
3961
+ },
3962
+ /**
3963
+ * Returns uploaded bytes for file identified by id
3964
+ */
3965
+ getLoaded: function(id){
3966
+ return fileState[id].loaded || 0;
3967
+ },
3968
+ isValid: function(id) {
3969
+ return fileState[id] !== undefined;
3970
+ },
3971
+ reset: function() {
3972
+ fileState = [];
3973
+ },
3974
+ getUuid: function(id) {
3975
+ return fileState[id].uuid;
3976
+ },
3977
+ /**
3978
+ * Sends the file identified by id to the server
3979
+ */
3980
+ upload: function(id, retry){
3981
+ var name = this.getName(id);
3982
+
3983
+ options.onUpload(id, name);
3984
+
3985
+ if (chunkFiles) {
3986
+ handleFileChunkingUpload(id, retry);
3987
+ }
3988
+ else {
3989
+ handleStandardFileUpload(id);
3990
+ }
3991
+ },
3992
+ cancel: function(id){
3993
+ var xhr = fileState[id].xhr;
3994
+
3995
+ options.onCancel(id, this.getName(id));
3996
+
3997
+ if (xhr) {
3998
+ xhr.onreadystatechange = null;
3999
+ xhr.abort();
4000
+ }
4001
+
4002
+ if (resumeEnabled) {
4003
+ deletePersistedChunkData(id);
4004
+ }
4005
+
4006
+ delete fileState[id];
4007
+ },
4008
+ getResumableFilesData: function() {
4009
+ var matchingCookieNames = [],
4010
+ resumableFilesData = [];
4011
+
4012
+ if (chunkFiles && resumeEnabled) {
4013
+ if (resumeId === undefined) {
4014
+ matchingCookieNames = qq.getCookieNames(new RegExp("^qqfilechunk\\" + cookieItemDelimiter + ".+\\" +
4015
+ cookieItemDelimiter + "\\d+\\" + cookieItemDelimiter + options.chunking.partSize + "="));
4016
+ }
4017
+ else {
4018
+ matchingCookieNames = qq.getCookieNames(new RegExp("^qqfilechunk\\" + cookieItemDelimiter + ".+\\" +
4019
+ cookieItemDelimiter + "\\d+\\" + cookieItemDelimiter + options.chunking.partSize + "\\" +
4020
+ cookieItemDelimiter + resumeId + "="));
4021
+ }
4022
+
4023
+ qq.each(matchingCookieNames, function(idx, cookieName) {
4024
+ var cookiesNameParts = cookieName.split(cookieItemDelimiter);
4025
+ var cookieValueParts = qq.getCookie(cookieName).split(cookieItemDelimiter);
4026
+
4027
+ resumableFilesData.push({
4028
+ name: decodeURIComponent(cookiesNameParts[1]),
4029
+ size: cookiesNameParts[2],
4030
+ uuid: cookieValueParts[0],
4031
+ partIdx: cookieValueParts[1]
4032
+ });
4033
+ });
4034
+
4035
+ return resumableFilesData;
4036
+ }
4037
+ return [];
4038
+ }
4039
+ };
4040
+
4041
+ return api;
4042
+ };
2431
4043
  /*globals jQuery, qq*/
2432
4044
  (function($) {
2433
4045
  "use strict";
@@ -2483,9 +4095,10 @@ qq.extend(qq.UploadHandlerXhr.prototype, {
2483
4095
  //implement all callbacks defined in Fine Uploader as functions that trigger appropriately names events and
2484
4096
  // return the result of executing the bound handler back to Fine Uploader
2485
4097
  addCallbacks = function(transformedOpts) {
2486
- var callbacks = transformedOpts.callbacks = {};
4098
+ var callbacks = transformedOpts.callbacks = {},
4099
+ uploaderInst = new qq.FineUploaderBasic();
2487
4100
 
2488
- $.each(new qq.FineUploaderBasic()._options.callbacks, function(prop, func) {
4101
+ $.each(uploaderInst._options.callbacks, function(prop, func) {
2489
4102
  var name, $callbackEl;
2490
4103
 
2491
4104
  name = /^on(\w+)/.exec(prop)[1];
@@ -2493,8 +4106,16 @@ qq.extend(qq.UploadHandlerXhr.prototype, {
2493
4106
  $callbackEl = $el;
2494
4107
 
2495
4108
  callbacks[prop] = function() {
2496
- var args = Array.prototype.slice.call(arguments);
2497
- return $callbackEl.triggerHandler(name, args);
4109
+ var origFunc = func,
4110
+ args = Array.prototype.slice.call(arguments),
4111
+ jqueryHandlerResult = $callbackEl.triggerHandler(name, args);
4112
+
4113
+ if (jqueryHandlerResult === undefined &&
4114
+ $.inArray(prop, uploaderInst.getPromissoryCallbackNames()) >= 0) {
4115
+ return origFunc();
4116
+ }
4117
+
4118
+ return jqueryHandlerResult;
2498
4119
  };
2499
4120
  });
2500
4121
  };