fileuploader-rails 3.1.1 → 3.4.1

Sign up to get free protection for your applications and to get access to all the features.
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
  };