fileuploader-rails 3.0.0 → 3.0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1976 @@
1
+ /**
2
+ * http://github.com/Valums-File-Uploader/file-uploader
3
+ *
4
+ * Multiple file upload component with progress-bar, drag-and-drop, support for all modern browsers.
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 )
8
+ *
9
+ * Licensed under MIT license, GNU GPL 2 or later, GNU LGPL 2 or later, see license.txt.
10
+ */
11
+
12
+ var qq = qq || {};
13
+ var qq = function(element) {
14
+ "use strict";
15
+
16
+ return {
17
+ hide: function() {
18
+ element.style.display = 'none';
19
+ return this;
20
+ },
21
+
22
+ /** Returns the function which detaches attached event */
23
+ attach: function(type, fn) {
24
+ if (element.addEventListener){
25
+ element.addEventListener(type, fn, false);
26
+ } else if (element.attachEvent){
27
+ element.attachEvent('on' + type, fn);
28
+ }
29
+ return function() {
30
+ qq(element).detach(type, fn);
31
+ };
32
+ },
33
+
34
+ detach: function(type, fn) {
35
+ if (element.removeEventListener){
36
+ element.removeEventListener(type, fn, false);
37
+ } else if (element.attachEvent){
38
+ element.detachEvent('on' + type, fn);
39
+ }
40
+ return this;
41
+ },
42
+
43
+ contains: function(descendant) {
44
+ // compareposition returns false in this case
45
+ if (element == descendant) {
46
+ return true;
47
+ }
48
+
49
+ if (element.contains){
50
+ return element.contains(descendant);
51
+ } else {
52
+ return !!(descendant.compareDocumentPosition(element) & 8);
53
+ }
54
+ },
55
+
56
+ /**
57
+ * Insert this element before elementB.
58
+ */
59
+ insertBefore: function(elementB) {
60
+ elementB.parentNode.insertBefore(element, elementB);
61
+ return this;
62
+ },
63
+
64
+ remove: function() {
65
+ element.parentNode.removeChild(element);
66
+ return this;
67
+ },
68
+
69
+ /**
70
+ * Sets styles for an element.
71
+ * Fixes opacity in IE6-8.
72
+ */
73
+ css: function(styles) {
74
+ if (styles.opacity != null){
75
+ if (typeof element.style.opacity != 'string' && typeof(element.filters) != 'undefined'){
76
+ styles.filter = 'alpha(opacity=' + Math.round(100 * styles.opacity) + ')';
77
+ }
78
+ }
79
+ qq.extend(element.style, styles);
80
+
81
+ return this;
82
+ },
83
+
84
+ hasClass: function(name) {
85
+ var re = new RegExp('(^| )' + name + '( |$)');
86
+ return re.test(element.className);
87
+ },
88
+
89
+ addClass: function(name) {
90
+ if (!qq(element).hasClass(name)){
91
+ element.className += ' ' + name;
92
+ }
93
+ return this;
94
+ },
95
+
96
+ removeClass: function(name) {
97
+ var re = new RegExp('(^| )' + name + '( |$)');
98
+ element.className = element.className.replace(re, ' ').replace(/^\s+|\s+$/g, "");
99
+ return this;
100
+ },
101
+
102
+ getByClass: function(className) {
103
+ if (element.querySelectorAll){
104
+ return element.querySelectorAll('.' + className);
105
+ }
106
+
107
+ var result = [];
108
+ var candidates = element.getElementsByTagName("*");
109
+ var len = candidates.length;
110
+
111
+ for (var i = 0; i < len; i++){
112
+ if (qq(candidates[i]).hasClass(className)){
113
+ result.push(candidates[i]);
114
+ }
115
+ }
116
+ return result;
117
+ },
118
+
119
+ children: function() {
120
+ var children = [],
121
+ child = element.firstChild;
122
+
123
+ while (child){
124
+ if (child.nodeType == 1){
125
+ children.push(child);
126
+ }
127
+ child = child.nextSibling;
128
+ }
129
+
130
+ return children;
131
+ },
132
+
133
+ setText: function(text) {
134
+ element.innerText = text;
135
+ element.textContent = text;
136
+ return this;
137
+ },
138
+
139
+ clearText: function() {
140
+ return qq(element).setText("");
141
+ }
142
+ };
143
+ };
144
+
145
+ qq.log = function(message, level) {
146
+ if (window.console) {
147
+ if (!level || level === 'info') {
148
+ window.console.log(message);
149
+ }
150
+ else
151
+ {
152
+ if (window.console[level]) {
153
+ window.console[level](message);
154
+ }
155
+ else {
156
+ window.console.log('<' + level + '> ' + message);
157
+ }
158
+ }
159
+ }
160
+ };
161
+
162
+ qq.isObject = function(variable) {
163
+ "use strict";
164
+ return variable !== null && variable && typeof(variable) === "object" && variable.constructor === Object;
165
+ };
166
+
167
+ qq.extend = function (first, second, extendNested) {
168
+ "use strict";
169
+ var prop;
170
+ for (prop in second) {
171
+ if (second.hasOwnProperty(prop)) {
172
+ if (extendNested && qq.isObject(second[prop])) {
173
+ if (first[prop] === undefined) {
174
+ first[prop] = {};
175
+ }
176
+ qq.extend(first[prop], second[prop], true);
177
+ }
178
+ else {
179
+ first[prop] = second[prop];
180
+ }
181
+ }
182
+ }
183
+ };
184
+
185
+ /**
186
+ * Searches for a given element in the array, returns -1 if it is not present.
187
+ * @param {Number} [from] The index at which to begin the search
188
+ */
189
+ qq.indexOf = function(arr, elt, from){
190
+ if (arr.indexOf) return arr.indexOf(elt, from);
191
+
192
+ from = from || 0;
193
+ var len = arr.length;
194
+
195
+ if (from < 0) from += len;
196
+
197
+ for (; from < len; from++){
198
+ if (from in arr && arr[from] === elt){
199
+ return from;
200
+ }
201
+ }
202
+ return -1;
203
+ };
204
+
205
+ qq.getUniqueId = (function(){
206
+ var id = 0;
207
+ return function(){ return id++; };
208
+ })();
209
+
210
+ //
211
+ // Browsers and platforms detection
212
+
213
+ qq.ie = function(){ return navigator.userAgent.indexOf('MSIE') != -1; }
214
+ qq.ie10 = function(){ return navigator.userAgent.indexOf('MSIE 10') != -1; }
215
+ qq.safari = function(){ return navigator.vendor != undefined && navigator.vendor.indexOf("Apple") != -1; }
216
+ qq.chrome = function(){ return navigator.vendor != undefined && navigator.vendor.indexOf('Google') != -1; }
217
+ qq.firefox = function(){ return (navigator.userAgent.indexOf('Mozilla') != -1 && navigator.vendor != undefined && navigator.vendor == ''); }
218
+ qq.windows = function(){ return navigator.platform == "Win32"; }
219
+
220
+ //
221
+ // Events
222
+
223
+ qq.preventDefault = function(e){
224
+ if (e.preventDefault){
225
+ e.preventDefault();
226
+ } else{
227
+ e.returnValue = false;
228
+ }
229
+ };
230
+
231
+ /**
232
+ * Creates and returns element from html string
233
+ * Uses innerHTML to create an element
234
+ */
235
+ qq.toElement = (function(){
236
+ var div = document.createElement('div');
237
+ return function(html){
238
+ div.innerHTML = html;
239
+ var element = div.firstChild;
240
+ div.removeChild(element);
241
+ return element;
242
+ };
243
+ })();
244
+
245
+ /**
246
+ * obj2url() takes a json-object as argument and generates
247
+ * a querystring. pretty much like jQuery.param()
248
+ *
249
+ * how to use:
250
+ *
251
+ * `qq.obj2url({a:'b',c:'d'},'http://any.url/upload?otherParam=value');`
252
+ *
253
+ * will result in:
254
+ *
255
+ * `http://any.url/upload?otherParam=value&a=b&c=d`
256
+ *
257
+ * @param Object JSON-Object
258
+ * @param String current querystring-part
259
+ * @return String encoded querystring
260
+ */
261
+ qq.obj2url = function(obj, temp, prefixDone){
262
+ var uristrings = [],
263
+ prefix = '&',
264
+ add = function(nextObj, i){
265
+ var nextTemp = temp
266
+ ? (/\[\]$/.test(temp)) // prevent double-encoding
267
+ ? temp
268
+ : temp+'['+i+']'
269
+ : i;
270
+ if ((nextTemp != 'undefined') && (i != 'undefined')) {
271
+ uristrings.push(
272
+ (typeof nextObj === 'object')
273
+ ? qq.obj2url(nextObj, nextTemp, true)
274
+ : (Object.prototype.toString.call(nextObj) === '[object Function]')
275
+ ? encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj())
276
+ : encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj)
277
+ );
278
+ }
279
+ };
280
+
281
+ if (!prefixDone && temp) {
282
+ prefix = (/\?/.test(temp)) ? (/\?$/.test(temp)) ? '' : '&' : '?';
283
+ uristrings.push(temp);
284
+ uristrings.push(qq.obj2url(obj));
285
+ } else if ((Object.prototype.toString.call(obj) === '[object Array]') && (typeof obj != 'undefined') ) {
286
+ // we wont use a for-in-loop on an array (performance)
287
+ for (var i = 0, len = obj.length; i < len; ++i){
288
+ add(obj[i], i);
289
+ }
290
+ } else if ((typeof obj != 'undefined') && (obj !== null) && (typeof obj === "object")){
291
+ // for anything else but a scalar, we will use for-in-loop
292
+ for (var i in obj){
293
+ add(obj[i], i);
294
+ }
295
+ } else {
296
+ uristrings.push(encodeURIComponent(temp) + '=' + encodeURIComponent(obj));
297
+ }
298
+
299
+ if (temp) {
300
+ return uristrings.join(prefix);
301
+ } else {
302
+ return uristrings.join(prefix)
303
+ .replace(/^&/, '')
304
+ .replace(/%20/g, '+');
305
+ }
306
+ };
307
+
308
+ /**
309
+ * A generic module which supports object disposing in dispose() method.
310
+ * */
311
+ qq.DisposeSupport = {
312
+ _disposers: [],
313
+
314
+ /** Run all registered disposers */
315
+ dispose: function() {
316
+ var disposer;
317
+ while (disposer = this._disposers.shift()) {
318
+ disposer();
319
+ }
320
+ },
321
+
322
+ /** Add disposer to the collection */
323
+ addDisposer: function(disposeFunction) {
324
+ this._disposers.push(disposeFunction);
325
+ },
326
+
327
+ /** Attach event handler and register de-attacher as a disposer */
328
+ _attach: function() {
329
+ this.addDisposer(qq(arguments[0]).attach.apply(this, Array.prototype.slice.call(arguments, 1)));
330
+ }
331
+ };
332
+ qq.UploadButton = function(o){
333
+ this._options = {
334
+ element: null,
335
+ // if set to true adds multiple attribute to file input
336
+ multiple: false,
337
+ acceptFiles: null,
338
+ // name attribute of file input
339
+ name: 'file',
340
+ onChange: function(input){},
341
+ hoverClass: 'qq-upload-button-hover',
342
+ focusClass: 'qq-upload-button-focus'
343
+ };
344
+
345
+ qq.extend(this._options, o);
346
+ qq.extend(this, qq.DisposeSupport);
347
+
348
+ this._element = this._options.element;
349
+
350
+ // make button suitable container for input
351
+ qq(this._element).css({
352
+ position: 'relative',
353
+ overflow: 'hidden',
354
+ // Make sure browse button is in the right side
355
+ // in Internet Explorer
356
+ direction: 'ltr'
357
+ });
358
+
359
+ this._input = this._createInput();
360
+ };
361
+
362
+ qq.UploadButton.prototype = {
363
+ /* returns file input element */
364
+ getInput: function(){
365
+ return this._input;
366
+ },
367
+ /* cleans/recreates the file input */
368
+ reset: function(){
369
+ if (this._input.parentNode){
370
+ qq(this._input).remove();
371
+ }
372
+
373
+ qq(this._element).removeClass(this._options.focusClass);
374
+ this._input = this._createInput();
375
+ },
376
+ _createInput: function(){
377
+ var input = document.createElement("input");
378
+
379
+ if (this._options.multiple){
380
+ input.setAttribute("multiple", "multiple");
381
+ }
382
+
383
+ if (this._options.acceptFiles) input.setAttribute("accept", this._options.acceptFiles);
384
+
385
+ input.setAttribute("type", "file");
386
+ input.setAttribute("name", this._options.name);
387
+
388
+ qq(input).css({
389
+ position: 'absolute',
390
+ // in Opera only 'browse' button
391
+ // is clickable and it is located at
392
+ // the right side of the input
393
+ right: 0,
394
+ top: 0,
395
+ fontFamily: 'Arial',
396
+ // 4 persons reported this, the max values that worked for them were 243, 236, 236, 118
397
+ fontSize: '118px',
398
+ margin: 0,
399
+ padding: 0,
400
+ cursor: 'pointer',
401
+ opacity: 0
402
+ });
403
+
404
+ this._element.appendChild(input);
405
+
406
+ var self = this;
407
+ this._attach(input, 'change', function(){
408
+ self._options.onChange(input);
409
+ });
410
+
411
+ this._attach(input, 'mouseover', function(){
412
+ qq(self._element).addClass(self._options.hoverClass);
413
+ });
414
+ this._attach(input, 'mouseout', function(){
415
+ qq(self._element).removeClass(self._options.hoverClass);
416
+ });
417
+ this._attach(input, 'focus', function(){
418
+ qq(self._element).addClass(self._options.focusClass);
419
+ });
420
+ this._attach(input, 'blur', function(){
421
+ qq(self._element).removeClass(self._options.focusClass);
422
+ });
423
+
424
+ // IE and Opera, unfortunately have 2 tab stops on file input
425
+ // which is unacceptable in our case, disable keyboard access
426
+ if (window.attachEvent){
427
+ // it is IE or Opera
428
+ input.setAttribute('tabIndex', "-1");
429
+ }
430
+
431
+ return input;
432
+ }
433
+ };
434
+ qq.FineUploaderBasic = function(o){
435
+ var that = this;
436
+ this._options = {
437
+ debug: false,
438
+ button: null,
439
+ multiple: true,
440
+ maxConnections: 3,
441
+ disableCancelForFormUploads: false,
442
+ autoUpload: true,
443
+ request: {
444
+ endpoint: '/server/upload',
445
+ params: {},
446
+ customHeaders: {},
447
+ forceMultipart: false,
448
+ inputName: 'qqfile'
449
+ },
450
+ validation: {
451
+ allowedExtensions: [],
452
+ sizeLimit: 0,
453
+ minSizeLimit: 0,
454
+ stopOnFirstInvalidFile: true
455
+ },
456
+ callbacks: {
457
+ onSubmit: function(id, fileName){}, // return false to cancel submit
458
+ onComplete: function(id, fileName, responseJSON){},
459
+ onCancel: function(id, fileName){},
460
+ onUpload: function(id, fileName, xhr){},
461
+ onProgress: function(id, fileName, loaded, total){},
462
+ onError: function(id, fileName, reason) {},
463
+ onAutoRetry: function(id, fileName, attemptNumber) {},
464
+ onManualRetry: function(id, fileName) {},
465
+ onValidate: function(fileData) {} // return false to prevent upload
466
+ },
467
+ messages: {
468
+ typeError: "{file} has an invalid extension. Valid extension(s): {extensions}.",
469
+ sizeError: "{file} is too large, maximum file size is {sizeLimit}.",
470
+ minSizeError: "{file} is too small, minimum file size is {minSizeLimit}.",
471
+ emptyError: "{file} is empty, please select files again without it.",
472
+ noFilesError: "No files to upload.",
473
+ onLeave: "The files are being uploaded, if you leave now the upload will be cancelled."
474
+ },
475
+ retry: {
476
+ enableAuto: false,
477
+ maxAutoAttempts: 3,
478
+ autoAttemptDelay: 5,
479
+ preventRetryResponseProperty: 'preventRetry'
480
+ }
481
+ };
482
+
483
+ qq.extend(this._options, o, true);
484
+ this._wrapCallbacks();
485
+ qq.extend(this, qq.DisposeSupport);
486
+
487
+ // number of files being uploaded
488
+ this._filesInProgress = 0;
489
+
490
+ this._storedFileIds = [];
491
+
492
+ this._autoRetries = [];
493
+ this._retryTimeouts = [];
494
+ this._preventRetries = [];
495
+
496
+ this._handler = this._createUploadHandler();
497
+
498
+ if (this._options.button){
499
+ this._button = this._createUploadButton(this._options.button);
500
+ }
501
+
502
+ this._preventLeaveInProgress();
503
+ };
504
+
505
+ qq.FineUploaderBasic.prototype = {
506
+ log: function(str, level) {
507
+ if (this._options.debug && (!level || level === 'info')) {
508
+ qq.log('[FineUploader] ' + str);
509
+ }
510
+ else if (level && level !== 'info') {
511
+ qq.log('[FineUploader] ' + str, level);
512
+
513
+ }
514
+ },
515
+ setParams: function(params){
516
+ this._options.request.params = params;
517
+ },
518
+ getInProgress: function(){
519
+ return this._filesInProgress;
520
+ },
521
+ uploadStoredFiles: function(){
522
+ "use strict";
523
+ while(this._storedFileIds.length) {
524
+ this._filesInProgress++;
525
+ this._handler.upload(this._storedFileIds.shift(), this._options.request.params);
526
+ }
527
+ },
528
+ clearStoredFiles: function(){
529
+ this._storedFileIds = [];
530
+ },
531
+ retry: function(id) {
532
+ if (this._onBeforeManualRetry(id)) {
533
+ this._handler.retry(id);
534
+ return true;
535
+ }
536
+ else {
537
+ return false;
538
+ }
539
+ },
540
+ cancel: function(fileId) {
541
+ this._handler.cancel(fileId);
542
+ },
543
+ reset: function() {
544
+ this.log("Resetting uploader...");
545
+ this._handler.reset();
546
+ this._filesInProgress = 0;
547
+ this._storedFileIds = [];
548
+ this._autoRetries = [];
549
+ this._retryTimeouts = [];
550
+ this._preventRetries = [];
551
+ this._button.reset();
552
+ },
553
+ _createUploadButton: function(element){
554
+ var self = this;
555
+
556
+ var button = new qq.UploadButton({
557
+ element: element,
558
+ multiple: this._options.multiple && qq.UploadHandlerXhr.isSupported(),
559
+ acceptFiles: this._options.validation.acceptFiles,
560
+ onChange: function(input){
561
+ self._onInputChange(input);
562
+ }
563
+ });
564
+
565
+ this.addDisposer(function() { button.dispose(); });
566
+ return button;
567
+ },
568
+ _createUploadHandler: function(){
569
+ var self = this,
570
+ handlerClass;
571
+
572
+ if(qq.UploadHandlerXhr.isSupported()){
573
+ handlerClass = 'UploadHandlerXhr';
574
+ } else {
575
+ handlerClass = 'UploadHandlerForm';
576
+ }
577
+
578
+ var handler = new qq[handlerClass]({
579
+ debug: this._options.debug,
580
+ endpoint: this._options.request.endpoint,
581
+ forceMultipart: this._options.request.forceMultipart,
582
+ maxConnections: this._options.maxConnections,
583
+ customHeaders: this._options.request.customHeaders,
584
+ inputName: this._options.request.inputName,
585
+ demoMode: this._options.demoMode,
586
+ log: this.log,
587
+ onProgress: function(id, fileName, loaded, total){
588
+ self._onProgress(id, fileName, loaded, total);
589
+ self._options.callbacks.onProgress(id, fileName, loaded, total);
590
+ },
591
+ onComplete: function(id, fileName, result, xhr){
592
+ self._onComplete(id, fileName, result, xhr);
593
+ self._options.callbacks.onComplete(id, fileName, result);
594
+ },
595
+ onCancel: function(id, fileName){
596
+ self._onCancel(id, fileName);
597
+ self._options.callbacks.onCancel(id, fileName);
598
+ },
599
+ onUpload: function(id, fileName, xhr){
600
+ self._onUpload(id, fileName, xhr);
601
+ self._options.callbacks.onUpload(id, fileName, xhr);
602
+ },
603
+ onAutoRetry: function(id, fileName, responseJSON, xhr) {
604
+ self._preventRetries[id] = responseJSON[self._options.retry.preventRetryResponseProperty];
605
+
606
+ if (self._shouldAutoRetry(id, fileName, responseJSON)) {
607
+ self._maybeParseAndSendUploadError(id, fileName, responseJSON, xhr);
608
+ self._options.callbacks.onAutoRetry(id, fileName, self._autoRetries[id] + 1);
609
+ self._onBeforeAutoRetry(id, fileName);
610
+
611
+ self._retryTimeouts[id] = setTimeout(function() {
612
+ self._onAutoRetry(id, fileName, responseJSON)
613
+ }, self._options.retry.autoAttemptDelay * 1000);
614
+
615
+ return true;
616
+ }
617
+ else {
618
+ return false;
619
+ }
620
+ }
621
+ });
622
+
623
+ return handler;
624
+ },
625
+ _preventLeaveInProgress: function(){
626
+ var self = this;
627
+
628
+ this._attach(window, 'beforeunload', function(e){
629
+ if (!self._filesInProgress){return;}
630
+
631
+ var e = e || window.event;
632
+ // for ie, ff
633
+ e.returnValue = self._options.messages.onLeave;
634
+ // for webkit
635
+ return self._options.messages.onLeave;
636
+ });
637
+ },
638
+ _onSubmit: function(id, fileName){
639
+ if (this._options.autoUpload) {
640
+ this._filesInProgress++;
641
+ }
642
+ },
643
+ _onProgress: function(id, fileName, loaded, total){
644
+ },
645
+ _onComplete: function(id, fileName, result, xhr){
646
+ this._filesInProgress--;
647
+ this._maybeParseAndSendUploadError(id, fileName, result, xhr);
648
+ },
649
+ _onCancel: function(id, fileName){
650
+ clearTimeout(this._retryTimeouts[id]);
651
+
652
+ var storedFileIndex = qq.indexOf(this._storedFileIds, id);
653
+ if (this._options.autoUpload || storedFileIndex < 0) {
654
+ this._filesInProgress--;
655
+ }
656
+ else if (!this._options.autoUpload) {
657
+ this._storedFileIds.splice(storedFileIndex, 1);
658
+ }
659
+ },
660
+ _onUpload: function(id, fileName, xhr){
661
+ },
662
+ _onInputChange: function(input){
663
+ if (this._handler instanceof qq.UploadHandlerXhr){
664
+ this._uploadFileList(input.files);
665
+ } else {
666
+ if (this._validateFile(input)){
667
+ this._uploadFile(input);
668
+ }
669
+ }
670
+ this._button.reset();
671
+ },
672
+ _onBeforeAutoRetry: function(id, fileName) {
673
+ this.log("Waiting " + this._options.retry.autoAttemptDelay + " seconds before retrying " + fileName + "...");
674
+ },
675
+ _onAutoRetry: function(id, fileName, responseJSON) {
676
+ this.log("Retrying " + fileName + "...");
677
+ this._autoRetries[id]++;
678
+ this._handler.retry(id);
679
+ },
680
+ _shouldAutoRetry: function(id, fileName, responseJSON) {
681
+ if (!this._preventRetries[id] && this._options.retry.enableAuto) {
682
+ if (this._autoRetries[id] === undefined) {
683
+ this._autoRetries[id] = 0;
684
+ }
685
+
686
+ return this._autoRetries[id] < this._options.retry.maxAutoAttempts
687
+ }
688
+
689
+ return false;
690
+ },
691
+ //return false if we should not attempt the requested retry
692
+ _onBeforeManualRetry: function(id) {
693
+ if (this._preventRetries[id]) {
694
+ this.log("Retries are forbidden for id " + id, 'warn');
695
+ return false;
696
+ }
697
+ else if (this._handler.isValid(id)) {
698
+ var fileName = this._handler.getName(id);
699
+
700
+ if (this._options.callbacks.onManualRetry(id, fileName) === false) {
701
+ return false;
702
+ }
703
+
704
+ this.log("Retrying upload for '" + fileName + "' (id: " + id + ")...");
705
+ this._filesInProgress++;
706
+ return true;
707
+ }
708
+ else {
709
+ this.log("'" + id + "' is not a valid file ID", 'error');
710
+ return false;
711
+ }
712
+ },
713
+ _maybeParseAndSendUploadError: function(id, fileName, response, xhr) {
714
+ //assuming no one will actually set the response code to something other than 200 and still set 'success' to true
715
+ if (!response.success){
716
+ if (xhr && xhr.status !== 200 && !response.error) {
717
+ this._options.callbacks.onError(id, fileName, "XHR returned response code " + xhr.status);
718
+ }
719
+ else {
720
+ var errorReason = response.error ? response.error : "Upload failure reason unknown";
721
+ this._options.callbacks.onError(id, fileName, errorReason);
722
+ }
723
+ }
724
+ },
725
+ _uploadFileList: function(files){
726
+ var validationDescriptors, index, batchInvalid;
727
+
728
+ validationDescriptors = this._getValidationDescriptors(files);
729
+ if (validationDescriptors.length > 1) {
730
+ batchInvalid = this._options.callbacks.onValidate(validationDescriptors) === false;
731
+ }
732
+
733
+ if (!batchInvalid) {
734
+ if (files.length > 0) {
735
+ for (index = 0; index < files.length; index++){
736
+ if (this._validateFile(files[index])){
737
+ this._uploadFile(files[index]);
738
+ } else {
739
+ if (this._options.validation.stopOnFirstInvalidFile){
740
+ return;
741
+ }
742
+ }
743
+ }
744
+ }
745
+ else {
746
+ this._error('noFilesError', "");
747
+ }
748
+ }
749
+ },
750
+ _uploadFile: function(fileContainer){
751
+ var id = this._handler.add(fileContainer);
752
+ var fileName = this._handler.getName(id);
753
+
754
+ if (this._options.callbacks.onSubmit(id, fileName) !== false){
755
+ this._onSubmit(id, fileName);
756
+ if (this._options.autoUpload) {
757
+ this._handler.upload(id, this._options.request.params);
758
+ }
759
+ else {
760
+ this._storeFileForLater(id);
761
+ }
762
+ }
763
+ },
764
+ _storeFileForLater: function(id) {
765
+ this._storedFileIds.push(id);
766
+ },
767
+ _validateFile: function(file){
768
+ var validationDescriptor, name, size;
769
+
770
+ validationDescriptor = this._getValidationDescriptor(file);
771
+ name = validationDescriptor.name;
772
+ size = validationDescriptor.size;
773
+
774
+ if (this._options.callbacks.onValidate([validationDescriptor]) === false) {
775
+ return false;
776
+ }
777
+
778
+ if (!this._isAllowedExtension(name)){
779
+ this._error('typeError', name);
780
+ return false;
781
+
782
+ }
783
+ else if (size === 0){
784
+ this._error('emptyError', name);
785
+ return false;
786
+
787
+ }
788
+ else if (size && this._options.validation.sizeLimit && size > this._options.validation.sizeLimit){
789
+ this._error('sizeError', name);
790
+ return false;
791
+
792
+ }
793
+ else if (size && size < this._options.validation.minSizeLimit){
794
+ this._error('minSizeError', name);
795
+ return false;
796
+ }
797
+
798
+ return true;
799
+ },
800
+ _error: function(code, fileName){
801
+ var message = this._options.messages[code];
802
+ function r(name, replacement){ message = message.replace(name, replacement); }
803
+
804
+ var extensions = this._options.validation.allowedExtensions.join(', ');
805
+
806
+ r('{file}', this._formatFileName(fileName));
807
+ r('{extensions}', extensions);
808
+ r('{sizeLimit}', this._formatSize(this._options.validation.sizeLimit));
809
+ r('{minSizeLimit}', this._formatSize(this._options.validation.minSizeLimit));
810
+
811
+ this._options.callbacks.onError(null, fileName, message);
812
+
813
+ return message;
814
+ },
815
+ _formatFileName: function(name){
816
+ if (name.length > 33){
817
+ name = name.slice(0, 19) + '...' + name.slice(-13);
818
+ }
819
+ return name;
820
+ },
821
+ _isAllowedExtension: function(fileName){
822
+ var ext = (-1 !== fileName.indexOf('.'))
823
+ ? fileName.replace(/.*[.]/, '').toLowerCase()
824
+ : '';
825
+ var allowed = this._options.validation.allowedExtensions;
826
+
827
+ if (!allowed.length){return true;}
828
+
829
+ for (var i=0; i<allowed.length; i++){
830
+ if (allowed[i].toLowerCase() == ext){ return true;}
831
+ }
832
+
833
+ return false;
834
+ },
835
+ _formatSize: function(bytes){
836
+ var i = -1;
837
+ do {
838
+ bytes = bytes / 1024;
839
+ i++;
840
+ } while (bytes > 99);
841
+
842
+ return Math.max(bytes, 0.1).toFixed(1) + ['kB', 'MB', 'GB', 'TB', 'PB', 'EB'][i];
843
+ },
844
+ _wrapCallbacks: function() {
845
+ var self, safeCallback;
846
+
847
+ self = this;
848
+
849
+ safeCallback = function(name, callback, args) {
850
+ try {
851
+ return callback.apply(self, args);
852
+ }
853
+ catch (exception) {
854
+ self.log("Caught exception in '" + name + "' callback - " + exception, 'error');
855
+ }
856
+ }
857
+
858
+ for (var prop in this._options.callbacks) {
859
+ (function() {
860
+ var oldCallback = self._options.callbacks[prop];
861
+ self._options.callbacks[prop] = function() {
862
+ return safeCallback(prop, oldCallback, arguments);
863
+ }
864
+ }());
865
+ }
866
+ },
867
+ _parseFileName: function(file) {
868
+ var name;
869
+
870
+ if (file.value){
871
+ // it is a file input
872
+ // get input value and remove path to normalize
873
+ name = file.value.replace(/.*(\/|\\)/, "");
874
+ } else {
875
+ // fix missing properties in Safari 4 and firefox 11.0a2
876
+ name = (file.fileName !== null && file.fileName !== undefined) ? file.fileName : file.name;
877
+ }
878
+
879
+ return name;
880
+ },
881
+ _parseFileSize: function(file) {
882
+ var size;
883
+
884
+ if (!file.value){
885
+ // fix missing properties in Safari 4 and firefox 11.0a2
886
+ size = (file.fileSize !== null && file.fileSize !== undefined) ? file.fileSize : file.size;
887
+ }
888
+
889
+ return size;
890
+ },
891
+ _getValidationDescriptor: function(file) {
892
+ var name, size, fileDescriptor;
893
+
894
+ fileDescriptor = {};
895
+ name = this._parseFileName(file);
896
+ size = this._parseFileSize(file);
897
+
898
+ fileDescriptor.name = name;
899
+ if (size) {
900
+ fileDescriptor.size = size;
901
+ }
902
+
903
+ return fileDescriptor;
904
+ },
905
+ _getValidationDescriptors: function(files) {
906
+ var index, fileDescriptors;
907
+
908
+ fileDescriptors = [];
909
+
910
+ for (index = 0; index < files.length; index++) {
911
+ fileDescriptors.push(files[index]);
912
+ }
913
+
914
+ return fileDescriptors;
915
+ }
916
+ };
917
+ /**
918
+ * Class that creates upload widget with drag-and-drop and file list
919
+ * @inherits qq.FineUploaderBasic
920
+ */
921
+ qq.FineUploader = function(o){
922
+ // call parent constructor
923
+ qq.FineUploaderBasic.apply(this, arguments);
924
+
925
+ // additional options
926
+ qq.extend(this._options, {
927
+ element: null,
928
+ listElement: null,
929
+ dragAndDrop: {
930
+ extraDropzones: [],
931
+ hideDropzones: true,
932
+ disableDefaultDropzone: false
933
+ },
934
+ text: {
935
+ uploadButton: 'Upload a file',
936
+ cancelButton: 'Cancel',
937
+ retryButton: 'Retry',
938
+ failUpload: 'Upload failed',
939
+ dragZone: 'Drop files here to upload',
940
+ formatProgress: "{percent}% of {total_size}",
941
+ waitingForResponse: "Processing..."
942
+ },
943
+ template: '<div class="qq-uploader">' +
944
+ ((!this._options.dragAndDrop || !this._options.dragAndDrop.disableDefaultDropzone) ? '<div class="qq-upload-drop-area"><span>{dragZoneText}</span></div>' : '') +
945
+ (!this._options.button ? '<div class="qq-upload-button"><div>{uploadButtonText}</div></div>' : '') +
946
+ (!this._options.listElement ? '<ul class="qq-upload-list"></ul>' : '') +
947
+ '</div>',
948
+
949
+ // template for one item in file list
950
+ fileTemplate: '<li>' +
951
+ '<div class="qq-progress-bar"></div>' +
952
+ '<span class="qq-upload-spinner"></span>' +
953
+ '<span class="qq-upload-finished"></span>' +
954
+ '<span class="qq-upload-file"></span>' +
955
+ '<span class="qq-upload-size"></span>' +
956
+ '<a class="qq-upload-cancel" href="#">{cancelButtonText}</a>' +
957
+ '<a class="qq-upload-retry" href="#">{retryButtonText}</a>' +
958
+ '<span class="qq-upload-status-text">{statusText}</span>' +
959
+ '</li>',
960
+ classes: {
961
+ // used to get elements from templates
962
+ button: 'qq-upload-button',
963
+ drop: 'qq-upload-drop-area',
964
+ dropActive: 'qq-upload-drop-area-active',
965
+ dropDisabled: 'qq-upload-drop-area-disabled',
966
+ list: 'qq-upload-list',
967
+ progressBar: 'qq-progress-bar',
968
+ file: 'qq-upload-file',
969
+ spinner: 'qq-upload-spinner',
970
+ finished: 'qq-upload-finished',
971
+ retrying: 'qq-upload-retrying',
972
+ retryable: 'qq-upload-retryable',
973
+ size: 'qq-upload-size',
974
+ cancel: 'qq-upload-cancel',
975
+ retry: 'qq-upload-retry',
976
+ statusText: 'qq-upload-status-text',
977
+
978
+ // added to list item <li> when upload completes
979
+ // used in css to hide progress spinner
980
+ success: 'qq-upload-success',
981
+ fail: 'qq-upload-fail',
982
+
983
+ successIcon: null,
984
+ failIcon: null
985
+ },
986
+ failedUploadTextDisplay: {
987
+ mode: 'default', //default, custom, or none
988
+ maxChars: 50,
989
+ responseProperty: 'error',
990
+ enableTooltip: true
991
+ },
992
+ messages: {
993
+ tooManyFilesError: "You may only drop one file"
994
+ },
995
+ retry: {
996
+ showAutoRetryNote: true,
997
+ autoRetryNote: "Retrying {retryNum}/{maxAuto}...",
998
+ showButton: false
999
+ },
1000
+ showMessage: function(message){
1001
+ alert(message);
1002
+ }
1003
+ }, true);
1004
+
1005
+ // overwrite options with user supplied
1006
+ qq.extend(this._options, o, true);
1007
+ this._wrapCallbacks();
1008
+
1009
+ // overwrite the upload button text if any
1010
+ // same for the Cancel button and Fail message text
1011
+ this._options.template = this._options.template.replace(/\{dragZoneText\}/g, this._options.text.dragZone);
1012
+ this._options.template = this._options.template.replace(/\{uploadButtonText\}/g, this._options.text.uploadButton);
1013
+ this._options.fileTemplate = this._options.fileTemplate.replace(/\{cancelButtonText\}/g, this._options.text.cancelButton);
1014
+ this._options.fileTemplate = this._options.fileTemplate.replace(/\{retryButtonText\}/g, this._options.text.retryButton);
1015
+ this._options.fileTemplate = this._options.fileTemplate.replace(/\{statusText\}/g, "");
1016
+
1017
+ this._element = this._options.element;
1018
+ this._element.innerHTML = this._options.template;
1019
+ this._listElement = this._options.listElement || this._find(this._element, 'list');
1020
+
1021
+ this._classes = this._options.classes;
1022
+
1023
+ if (!this._button) {
1024
+ this._button = this._createUploadButton(this._find(this._element, 'button'));
1025
+ }
1026
+
1027
+ this._bindCancelAndRetryEvents();
1028
+ this._setupDragDrop();
1029
+ };
1030
+
1031
+ // inherit from Basic Uploader
1032
+ qq.extend(qq.FineUploader.prototype, qq.FineUploaderBasic.prototype);
1033
+
1034
+ qq.extend(qq.FineUploader.prototype, {
1035
+ clearStoredFiles: function() {
1036
+ qq.FineUploaderBasic.prototype.clearStoredFiles.apply(this, arguments);
1037
+ this._listElement.innerHTML = "";
1038
+ },
1039
+ addExtraDropzone: function(element){
1040
+ this._setupExtraDropzone(element);
1041
+ },
1042
+ removeExtraDropzone: function(element){
1043
+ var dzs = this._options.dragAndDrop.extraDropzones;
1044
+ for(var i in dzs) if (dzs[i] === element) return this._options.dragAndDrop.extraDropzones.splice(i,1);
1045
+ },
1046
+ getItemByFileId: function(id){
1047
+ var item = this._listElement.firstChild;
1048
+
1049
+ // there can't be txt nodes in dynamically created list
1050
+ // and we can use nextSibling
1051
+ while (item){
1052
+ if (item.qqFileId == id) return item;
1053
+ item = item.nextSibling;
1054
+ }
1055
+ },
1056
+ reset: function() {
1057
+ qq.FineUploaderBasic.prototype.reset.apply(this, arguments);
1058
+ this._element.innerHTML = this._options.template;
1059
+ this._listElement = this._options.listElement || this._find(this._element, 'list');
1060
+ if (!this._options.button) {
1061
+ this._button = this._createUploadButton(this._find(this._element, 'button'));
1062
+ }
1063
+ this._bindCancelAndRetryEvents();
1064
+ this._setupDragDrop();
1065
+ },
1066
+ _leaving_document_out: function(e){
1067
+ return ((qq.chrome() || (qq.safari() && qq.windows())) && e.clientX == 0 && e.clientY == 0) // null coords for Chrome and Safari Windows
1068
+ || (qq.firefox() && !e.relatedTarget); // null e.relatedTarget for Firefox
1069
+ },
1070
+ _storeFileForLater: function(id) {
1071
+ qq.FineUploaderBasic.prototype._storeFileForLater.apply(this, arguments);
1072
+ var item = this.getItemByFileId(id);
1073
+ qq(this._find(item, 'spinner')).hide();
1074
+ },
1075
+ /**
1076
+ * Gets one of the elements listed in this._options.classes
1077
+ **/
1078
+ _find: function(parent, type){
1079
+ var element = qq(parent).getByClass(this._options.classes[type])[0];
1080
+ if (!element){
1081
+ throw new Error('element not found ' + type);
1082
+ }
1083
+
1084
+ return element;
1085
+ },
1086
+ _setupExtraDropzone: function(element){
1087
+ this._options.dragAndDrop.extraDropzones.push(element);
1088
+ this._setupDropzone(element);
1089
+ },
1090
+ _setupDropzone: function(dropArea){
1091
+ var self = this;
1092
+
1093
+ var dz = new qq.UploadDropZone({
1094
+ element: dropArea,
1095
+ onEnter: function(e){
1096
+ qq(dropArea).addClass(self._classes.dropActive);
1097
+ e.stopPropagation();
1098
+ },
1099
+ onLeave: function(e){
1100
+ //e.stopPropagation();
1101
+ },
1102
+ onLeaveNotDescendants: function(e){
1103
+ qq(dropArea).removeClass(self._classes.dropActive);
1104
+ },
1105
+ onDrop: function(e){
1106
+ if (self._options.dragAndDrop.hideDropzones) {
1107
+ qq(dropArea).hide();
1108
+ }
1109
+
1110
+ qq(dropArea).removeClass(self._classes.dropActive);
1111
+ if (e.dataTransfer.files.length > 1 && !self._options.multiple) {
1112
+ self._error('tooManyFilesError', "");
1113
+ }
1114
+ else {
1115
+ self._uploadFileList(e.dataTransfer.files);
1116
+ }
1117
+ }
1118
+ });
1119
+
1120
+ this.addDisposer(function() { dz.dispose(); });
1121
+
1122
+ if (this._options.dragAndDrop.hideDropzones) {
1123
+ qq(dropArea).hide();
1124
+ }
1125
+ },
1126
+ _setupDragDrop: function(){
1127
+ var self, dropArea;
1128
+
1129
+ self = this;
1130
+
1131
+ if (!this._options.dragAndDrop.disableDefaultDropzone) {
1132
+ dropArea = this._find(this._element, 'drop');
1133
+ this._options.dragAndDrop.extraDropzones.push(dropArea);
1134
+ }
1135
+
1136
+ var dropzones = this._options.dragAndDrop.extraDropzones;
1137
+ var i;
1138
+ for (i=0; i < dropzones.length; i++){
1139
+ this._setupDropzone(dropzones[i]);
1140
+ }
1141
+
1142
+ // IE <= 9 does not support the File API used for drag+drop uploads
1143
+ if (!this._options.dragAndDrop.disableDefaultDropzone && (!qq.ie() || qq.ie10())) {
1144
+ this._attach(document, 'dragenter', function(e){
1145
+ if (qq(dropArea).hasClass(self._classes.dropDisabled)) return;
1146
+
1147
+ dropArea.style.display = 'block';
1148
+ for (i=0; i < dropzones.length; i++){ dropzones[i].style.display = 'block'; }
1149
+
1150
+ });
1151
+ }
1152
+ this._attach(document, 'dragleave', function(e){
1153
+ if (self._options.dragAndDrop.hideDropzones && qq.FineUploader.prototype._leaving_document_out(e)) {
1154
+ for (i=0; i < dropzones.length; i++) {
1155
+ qq(dropzones[i]).hide();
1156
+ }
1157
+ }
1158
+ });
1159
+ qq(document).attach('drop', function(e){
1160
+ if (self._options.dragAndDrop.hideDropzones) {
1161
+ for (i=0; i < dropzones.length; i++) {
1162
+ qq(dropzones[i]).hide();
1163
+ }
1164
+ }
1165
+ e.preventDefault();
1166
+ });
1167
+ },
1168
+ _onSubmit: function(id, fileName){
1169
+ qq.FineUploaderBasic.prototype._onSubmit.apply(this, arguments);
1170
+ this._addToList(id, fileName);
1171
+ },
1172
+ // Update the progress bar & percentage as the file is uploaded
1173
+ _onProgress: function(id, fileName, loaded, total){
1174
+ qq.FineUploaderBasic.prototype._onProgress.apply(this, arguments);
1175
+
1176
+ var item, progressBar, text, percent, cancelLink, size;
1177
+
1178
+ item = this.getItemByFileId(id);
1179
+ progressBar = this._find(item, 'progressBar');
1180
+ percent = Math.round(loaded / total * 100);
1181
+
1182
+ if (loaded === total) {
1183
+ cancelLink = this._find(item, 'cancel');
1184
+ qq(cancelLink).hide();
1185
+
1186
+ qq(progressBar).hide();
1187
+ qq(this._find(item, 'statusText')).setText(this._options.text.waitingForResponse);
1188
+
1189
+ // If last byte was sent, just display final size
1190
+ text = this._formatSize(total);
1191
+ }
1192
+ else {
1193
+ // If still uploading, display percentage
1194
+ text = this._formatProgress(loaded, total);
1195
+
1196
+ qq(progressBar).css({display: 'block'});
1197
+ }
1198
+
1199
+ // Update progress bar element
1200
+ qq(progressBar).css({width: percent + '%'});
1201
+
1202
+ size = this._find(item, 'size');
1203
+ qq(size).css({display: 'inline'});
1204
+ qq(size).setText(text);
1205
+ },
1206
+ _onComplete: function(id, fileName, result, xhr){
1207
+ qq.FineUploaderBasic.prototype._onComplete.apply(this, arguments);
1208
+
1209
+ var item = this.getItemByFileId(id);
1210
+
1211
+ qq(this._find(item, 'statusText')).clearText();
1212
+
1213
+ qq(item).removeClass(this._classes.retrying);
1214
+ qq(this._find(item, 'progressBar')).hide();
1215
+
1216
+ if (!this._options.disableCancelForFormUploads || qq.UploadHandlerXhr.isSupported()) {
1217
+ qq(this._find(item, 'cancel')).hide();
1218
+ }
1219
+ qq(this._find(item, 'spinner')).hide();
1220
+
1221
+ if (result.success){
1222
+ qq(item).addClass(this._classes.success);
1223
+ if (this._classes.successIcon) {
1224
+ this._find(item, 'finished').style.display = "inline-block";
1225
+ qq(item).addClass(this._classes.successIcon);
1226
+ }
1227
+ } else {
1228
+ qq(item).addClass(this._classes.fail);
1229
+ if (this._classes.failIcon) {
1230
+ this._find(item, 'finished').style.display = "inline-block";
1231
+ qq(item).addClass(this._classes.failIcon);
1232
+ }
1233
+ if (this._options.retry.showButton && !this._preventRetries[id]) {
1234
+ qq(item).addClass(this._classes.retryable);
1235
+ }
1236
+ this._controlFailureTextDisplay(item, result);
1237
+ }
1238
+ },
1239
+ _onUpload: function(id, fileName, xhr){
1240
+ qq.FineUploaderBasic.prototype._onUpload.apply(this, arguments);
1241
+
1242
+ var item = this.getItemByFileId(id);
1243
+ this._showSpinner(item);
1244
+ },
1245
+ _onBeforeAutoRetry: function(id) {
1246
+ var item, progressBar, cancelLink, failTextEl, retryNumForDisplay, maxAuto, retryNote;
1247
+
1248
+ qq.FineUploaderBasic.prototype._onBeforeAutoRetry.apply(this, arguments);
1249
+
1250
+ item = this.getItemByFileId(id);
1251
+ progressBar = this._find(item, 'progressBar');
1252
+
1253
+ this._showCancelLink(item);
1254
+ progressBar.style.width = 0;
1255
+ qq(progressBar).hide();
1256
+
1257
+ if (this._options.retry.showAutoRetryNote) {
1258
+ failTextEl = this._find(item, 'statusText');
1259
+ retryNumForDisplay = this._autoRetries[id] + 1;
1260
+ maxAuto = this._options.retry.maxAutoAttempts;
1261
+
1262
+ retryNote = this._options.retry.autoRetryNote.replace(/\{retryNum\}/g, retryNumForDisplay);
1263
+ retryNote = retryNote.replace(/\{maxAuto\}/g, maxAuto);
1264
+
1265
+ qq(failTextEl).setText(retryNote);
1266
+ if (retryNumForDisplay === 1) {
1267
+ qq(item).addClass(this._classes.retrying);
1268
+ }
1269
+ }
1270
+ },
1271
+ //return false if we should not attempt the requested retry
1272
+ _onBeforeManualRetry: function(id) {
1273
+ if (qq.FineUploaderBasic.prototype._onBeforeManualRetry.apply(this, arguments)) {
1274
+ var item = this.getItemByFileId(id);
1275
+ this._find(item, 'progressBar').style.width = 0;
1276
+ qq(item).removeClass(this._classes.fail);
1277
+ this._showSpinner(item);
1278
+ this._showCancelLink(item);
1279
+ return true;
1280
+ }
1281
+ return false;
1282
+ },
1283
+ _addToList: function(id, fileName){
1284
+ var item = qq.toElement(this._options.fileTemplate);
1285
+ if (this._options.disableCancelForFormUploads && !qq.UploadHandlerXhr.isSupported()) {
1286
+ var cancelLink = this._find(item, 'cancel');
1287
+ qq(cancelLink).remove();
1288
+ }
1289
+
1290
+ item.qqFileId = id;
1291
+
1292
+ var fileElement = this._find(item, 'file');
1293
+ qq(fileElement).setText(this._formatFileName(fileName));
1294
+ qq(this._find(item, 'size')).hide();
1295
+ if (!this._options.multiple) this._clearList();
1296
+ this._listElement.appendChild(item);
1297
+ },
1298
+ _clearList: function(){
1299
+ this._listElement.innerHTML = '';
1300
+ this.clearStoredFiles();
1301
+ },
1302
+ /**
1303
+ * delegate click event for cancel & retry links
1304
+ **/
1305
+ _bindCancelAndRetryEvents: function(){
1306
+ var self = this,
1307
+ list = this._listElement;
1308
+
1309
+ this._attach(list, 'click', function(e){
1310
+ e = e || window.event;
1311
+ var target = e.target || e.srcElement;
1312
+
1313
+ if (qq(target).hasClass(self._classes.cancel) || qq(target).hasClass(self._classes.retry)){
1314
+ qq.preventDefault(e);
1315
+
1316
+ var item = target.parentNode;
1317
+ while(item.qqFileId == undefined) {
1318
+ item = target = target.parentNode;
1319
+ }
1320
+
1321
+ if (qq(target).hasClass(self._classes.cancel)) {
1322
+ self.cancel(item.qqFileId);
1323
+ qq(item).remove();
1324
+ }
1325
+ else {
1326
+ qq(item).removeClass(self._classes.retryable);
1327
+ self.retry(item.qqFileId);
1328
+ }
1329
+ }
1330
+ });
1331
+ },
1332
+ _formatProgress: function (uploadedSize, totalSize) {
1333
+ var message = this._options.text.formatProgress;
1334
+ function r(name, replacement) { message = message.replace(name, replacement); }
1335
+
1336
+ r('{percent}', Math.round(uploadedSize / totalSize * 100));
1337
+ r('{total_size}', this._formatSize(totalSize));
1338
+ return message;
1339
+ },
1340
+ _controlFailureTextDisplay: function(item, response) {
1341
+ var mode, maxChars, responseProperty, failureReason, shortFailureReason;
1342
+
1343
+ mode = this._options.failedUploadTextDisplay.mode;
1344
+ maxChars = this._options.failedUploadTextDisplay.maxChars;
1345
+ responseProperty = this._options.failedUploadTextDisplay.responseProperty;
1346
+
1347
+ if (mode === 'custom') {
1348
+ failureReason = response[responseProperty];
1349
+ if (failureReason) {
1350
+ if (failureReason.length > maxChars) {
1351
+ shortFailureReason = failureReason.substring(0, maxChars) + '...';
1352
+ }
1353
+ }
1354
+ else {
1355
+ failureReason = this._options.text.failUpload;
1356
+ this.log("'" + responseProperty + "' is not a valid property on the server response.", 'warn');
1357
+ }
1358
+
1359
+ qq(this._find(item, 'statusText')).setText(shortFailureReason || failureReason);
1360
+
1361
+ if (this._options.failedUploadTextDisplay.enableTooltip) {
1362
+ this._showTooltip(item, failureReason);
1363
+ }
1364
+ }
1365
+ else if (mode === 'default') {
1366
+ qq(this._find(item, 'statusText')).setText(this._options.text.failUpload);
1367
+ }
1368
+ else if (mode !== 'none') {
1369
+ this.log("failedUploadTextDisplay.mode value of '" + mode + "' is not valid", 'warn');
1370
+ }
1371
+ },
1372
+ //TODO turn this into a real tooltip, with click trigger (so it is usable on mobile devices). See case #355 for details.
1373
+ _showTooltip: function(item, text) {
1374
+ item.title = text;
1375
+ },
1376
+ _showSpinner: function(item) {
1377
+ var spinnerEl = this._find(item, 'spinner');
1378
+ spinnerEl.style.display = "inline-block";
1379
+ },
1380
+ _showCancelLink: function(item) {
1381
+ if (!this._options.disableCancelForFormUploads || qq.UploadHandlerXhr.isSupported()) {
1382
+ var cancelLink = this._find(item, 'cancel');
1383
+ cancelLink.style.display = 'inline';
1384
+ }
1385
+ },
1386
+ _error: function(code, fileName){
1387
+ var message = qq.FineUploaderBasic.prototype._error.apply(this, arguments);
1388
+ this._options.showMessage(message);
1389
+ }
1390
+ });
1391
+
1392
+ qq.UploadDropZone = function(o){
1393
+ this._options = {
1394
+ element: null,
1395
+ onEnter: function(e){},
1396
+ onLeave: function(e){},
1397
+ // is not fired when leaving element by hovering descendants
1398
+ onLeaveNotDescendants: function(e){},
1399
+ onDrop: function(e){}
1400
+ };
1401
+ qq.extend(this._options, o);
1402
+ qq.extend(this, qq.DisposeSupport);
1403
+
1404
+ this._element = this._options.element;
1405
+
1406
+ this._disableDropOutside();
1407
+ this._attachEvents();
1408
+ };
1409
+
1410
+ qq.UploadDropZone.prototype = {
1411
+ _dragover_should_be_canceled: function(){
1412
+ return qq.safari() || (qq.firefox() && qq.windows());
1413
+ },
1414
+ _disableDropOutside: function(e){
1415
+ // run only once for all instances
1416
+ if (!qq.UploadDropZone.dropOutsideDisabled ){
1417
+
1418
+ // for these cases we need to catch onDrop to reset dropArea
1419
+ if (this._dragover_should_be_canceled){
1420
+ qq(document).attach('dragover', function(e){
1421
+ e.preventDefault();
1422
+ });
1423
+ } else {
1424
+ qq(document).attach('dragover', function(e){
1425
+ if (e.dataTransfer){
1426
+ e.dataTransfer.dropEffect = 'none';
1427
+ e.preventDefault();
1428
+ }
1429
+ });
1430
+ }
1431
+
1432
+ qq.UploadDropZone.dropOutsideDisabled = true;
1433
+ }
1434
+ },
1435
+ _attachEvents: function(){
1436
+ var self = this;
1437
+
1438
+ self._attach(self._element, 'dragover', function(e){
1439
+ if (!self._isValidFileDrag(e)) return;
1440
+
1441
+ var effect = qq.ie() ? null : e.dataTransfer.effectAllowed;
1442
+ if (effect == 'move' || effect == 'linkMove'){
1443
+ e.dataTransfer.dropEffect = 'move'; // for FF (only move allowed)
1444
+ } else {
1445
+ e.dataTransfer.dropEffect = 'copy'; // for Chrome
1446
+ }
1447
+
1448
+ e.stopPropagation();
1449
+ e.preventDefault();
1450
+ });
1451
+
1452
+ self._attach(self._element, 'dragenter', function(e){
1453
+ if (!self._isValidFileDrag(e)) return;
1454
+
1455
+ self._options.onEnter(e);
1456
+ });
1457
+
1458
+ self._attach(self._element, 'dragleave', function(e){
1459
+ if (!self._isValidFileDrag(e)) return;
1460
+
1461
+ self._options.onLeave(e);
1462
+
1463
+ var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);
1464
+ // do not fire when moving a mouse over a descendant
1465
+ if (qq(this).contains(relatedTarget)) return;
1466
+
1467
+ self._options.onLeaveNotDescendants(e);
1468
+ });
1469
+
1470
+ self._attach(self._element, 'drop', function(e){
1471
+ if (!self._isValidFileDrag(e)) return;
1472
+
1473
+ e.preventDefault();
1474
+ self._options.onDrop(e);
1475
+ });
1476
+ },
1477
+ _isValidFileDrag: function(e){
1478
+ // e.dataTransfer currently causing IE errors
1479
+ // IE9 does NOT support file API, so drag-and-drop is not possible
1480
+ if (qq.ie() && !qq.ie10()) return false;
1481
+
1482
+ var dt = e.dataTransfer,
1483
+ // do not check dt.types.contains in webkit, because it crashes safari 4
1484
+ isSafari = qq.safari();
1485
+
1486
+ // dt.effectAllowed is none in Safari 5
1487
+ // dt.types.contains check is for firefox
1488
+ var effectTest = qq.ie10() ? true : dt.effectAllowed != 'none';
1489
+ return dt && effectTest && (dt.files || (!isSafari && dt.types.contains && dt.types.contains('Files')));
1490
+ }
1491
+ };
1492
+ /**
1493
+ * Class for uploading files, uploading itself is handled by child classes
1494
+ */
1495
+ qq.UploadHandlerAbstract = function(o){
1496
+ // Default options, can be overridden by the user
1497
+ this._options = {
1498
+ debug: false,
1499
+ endpoint: '/upload.php',
1500
+ // maximum number of concurrent uploads
1501
+ maxConnections: 999,
1502
+ log: function(str, level) {},
1503
+ onProgress: function(id, fileName, loaded, total){},
1504
+ onComplete: function(id, fileName, response, xhr){},
1505
+ onCancel: function(id, fileName){},
1506
+ onUpload: function(id, fileName, xhr){},
1507
+ onAutoRetry: function(id, fileName, response, xhr){}
1508
+
1509
+ };
1510
+ qq.extend(this._options, o);
1511
+
1512
+ this._queue = [];
1513
+ // params for files in queue
1514
+ this._params = [];
1515
+
1516
+ this.log = this._options.log;
1517
+ };
1518
+ qq.UploadHandlerAbstract.prototype = {
1519
+ /**
1520
+ * Adds file or file input to the queue
1521
+ * @returns id
1522
+ **/
1523
+ add: function(file){},
1524
+ /**
1525
+ * Sends the file identified by id and additional query params to the server
1526
+ */
1527
+ upload: function(id, params){
1528
+ var len = this._queue.push(id);
1529
+
1530
+ var copy = {};
1531
+ qq.extend(copy, params);
1532
+ this._params[id] = copy;
1533
+
1534
+ // if too many active uploads, wait...
1535
+ if (len <= this._options.maxConnections){
1536
+ this._upload(id, this._params[id]);
1537
+ }
1538
+ },
1539
+ retry: function(id) {
1540
+ var i = qq.indexOf(this._queue, id);
1541
+ if (i >= 0) {
1542
+ this._upload(id, this._params[id]);
1543
+ }
1544
+ else {
1545
+ this.upload(id, this._params[id]);
1546
+ }
1547
+ },
1548
+ /**
1549
+ * Cancels file upload by id
1550
+ */
1551
+ cancel: function(id){
1552
+ this.log('Cancelling ' + id);
1553
+ this._cancel(id);
1554
+ this._dequeue(id);
1555
+ },
1556
+ /**
1557
+ * Cancells all uploads
1558
+ */
1559
+ cancelAll: function(){
1560
+ for (var i=0; i<this._queue.length; i++){
1561
+ this._cancel(this._queue[i]);
1562
+ }
1563
+ this._queue = [];
1564
+ },
1565
+ /**
1566
+ * Returns name of the file identified by id
1567
+ */
1568
+ getName: function(id){},
1569
+ /**
1570
+ * Returns size of the file identified by id
1571
+ */
1572
+ getSize: function(id){},
1573
+ /**
1574
+ * Returns id of files being uploaded or
1575
+ * waiting for their turn
1576
+ */
1577
+ getQueue: function(){
1578
+ return this._queue;
1579
+ },
1580
+ reset: function() {
1581
+ this.log('Resetting upload handler');
1582
+ this._queue = [];
1583
+ this._params = [];
1584
+ },
1585
+ /**
1586
+ * Actual upload method
1587
+ */
1588
+ _upload: function(id){},
1589
+ /**
1590
+ * Actual cancel method
1591
+ */
1592
+ _cancel: function(id){},
1593
+ /**
1594
+ * Removes element from queue, starts upload of next
1595
+ */
1596
+ _dequeue: function(id){
1597
+ var i = qq.indexOf(this._queue, id);
1598
+ this._queue.splice(i, 1);
1599
+
1600
+ var max = this._options.maxConnections;
1601
+
1602
+ if (this._queue.length >= max && i < max){
1603
+ var nextId = this._queue[max-1];
1604
+ this._upload(nextId, this._params[nextId]);
1605
+ }
1606
+ },
1607
+ /**
1608
+ * Determine if the file exists.
1609
+ */
1610
+ isValid: function(id) {}
1611
+ };
1612
+ /**
1613
+ * Class for uploading files using form and iframe
1614
+ * @inherits qq.UploadHandlerAbstract
1615
+ */
1616
+ qq.UploadHandlerForm = function(o){
1617
+ qq.UploadHandlerAbstract.apply(this, arguments);
1618
+
1619
+ this._inputs = {};
1620
+ this._detach_load_events = {};
1621
+ };
1622
+ // @inherits qq.UploadHandlerAbstract
1623
+ qq.extend(qq.UploadHandlerForm.prototype, qq.UploadHandlerAbstract.prototype);
1624
+
1625
+ qq.extend(qq.UploadHandlerForm.prototype, {
1626
+ add: function(fileInput){
1627
+ fileInput.setAttribute('name', this._options.inputName);
1628
+ var id = 'qq-upload-handler-iframe' + qq.getUniqueId();
1629
+
1630
+ this._inputs[id] = fileInput;
1631
+
1632
+ // remove file input from DOM
1633
+ if (fileInput.parentNode){
1634
+ qq(fileInput).remove();
1635
+ }
1636
+
1637
+ return id;
1638
+ },
1639
+ getName: function(id){
1640
+ // get input value and remove path to normalize
1641
+ return this._inputs[id].value.replace(/.*(\/|\\)/, "");
1642
+ },
1643
+ isValid: function(id) {
1644
+ return this._inputs[id] !== undefined;
1645
+ },
1646
+ reset: function() {
1647
+ qq.UploadHandlerAbstract.prototype.reset.apply(this, arguments);
1648
+ this._inputs = {};
1649
+ this._detach_load_events = {};
1650
+ },
1651
+ _cancel: function(id){
1652
+ this._options.onCancel(id, this.getName(id));
1653
+
1654
+ delete this._inputs[id];
1655
+ delete this._detach_load_events[id];
1656
+
1657
+ var iframe = document.getElementById(id);
1658
+ if (iframe){
1659
+ // to cancel request set src to something else
1660
+ // we use src="javascript:false;" because it doesn't
1661
+ // trigger ie6 prompt on https
1662
+ iframe.setAttribute('src', 'javascript:false;');
1663
+
1664
+ qq(iframe).remove();
1665
+ }
1666
+ },
1667
+ _upload: function(id, params){
1668
+ this._options.onUpload(id, this.getName(id), false);
1669
+ var input = this._inputs[id];
1670
+
1671
+ if (!input){
1672
+ throw new Error('file with passed id was not added, or already uploaded or cancelled');
1673
+ }
1674
+
1675
+ var fileName = this.getName(id);
1676
+ params[this._options.inputName] = fileName;
1677
+
1678
+ var iframe = this._createIframe(id);
1679
+ var form = this._createForm(iframe, params);
1680
+ form.appendChild(input);
1681
+
1682
+ var self = this;
1683
+ this._attachLoadEvent(iframe, function(){
1684
+ self.log('iframe loaded');
1685
+
1686
+ var response = self._getIframeContentJSON(iframe);
1687
+
1688
+ // timeout added to fix busy state in FF3.6
1689
+ setTimeout(function(){
1690
+ self._detach_load_events[id]();
1691
+ delete self._detach_load_events[id];
1692
+ qq(iframe).remove();
1693
+ }, 1);
1694
+
1695
+ if (!response.success) {
1696
+ if (self._options.onAutoRetry(id, fileName, response)) {
1697
+ return;
1698
+ }
1699
+ }
1700
+ self._options.onComplete(id, fileName, response);
1701
+ self._dequeue(id);
1702
+ });
1703
+
1704
+ this.log('Sending upload request for ' + id);
1705
+ form.submit();
1706
+ qq(form).remove();
1707
+
1708
+ return id;
1709
+ },
1710
+ _attachLoadEvent: function(iframe, callback){
1711
+ var self = this;
1712
+ this._detach_load_events[iframe.id] = qq(iframe).attach('load', function(){
1713
+ self.log('Received response for ' + iframe.id);
1714
+
1715
+ // when we remove iframe from dom
1716
+ // the request stops, but in IE load
1717
+ // event fires
1718
+ if (!iframe.parentNode){
1719
+ return;
1720
+ }
1721
+
1722
+ try {
1723
+ // fixing Opera 10.53
1724
+ if (iframe.contentDocument &&
1725
+ iframe.contentDocument.body &&
1726
+ iframe.contentDocument.body.innerHTML == "false"){
1727
+ // In Opera event is fired second time
1728
+ // when body.innerHTML changed from false
1729
+ // to server response approx. after 1 sec
1730
+ // when we upload file with iframe
1731
+ return;
1732
+ }
1733
+ }
1734
+ catch (error) {
1735
+ //IE may throw an "access is denied" error when attempting to access contentDocument on the iframe in some cases
1736
+ self.log('Error when attempting to access iframe during handling of upload response (' + error + ")", 'error');
1737
+ }
1738
+
1739
+ callback();
1740
+ });
1741
+ },
1742
+ /**
1743
+ * Returns json object received by iframe from server.
1744
+ */
1745
+ _getIframeContentJSON: function(iframe){
1746
+ //IE may throw an "access is denied" error when attempting to access contentDocument on the iframe in some cases
1747
+ try {
1748
+ // iframe.contentWindow.document - for IE<7
1749
+ var doc = iframe.contentDocument ? iframe.contentDocument: iframe.contentWindow.document,
1750
+ response;
1751
+
1752
+ var innerHTML = doc.body.innerHTML;
1753
+ this.log("converting iframe's innerHTML to JSON");
1754
+ this.log("innerHTML = " + innerHTML);
1755
+ //plain text response may be wrapped in <pre> tag
1756
+ if (innerHTML && innerHTML.match(/^<pre/i)) {
1757
+ innerHTML = doc.body.firstChild.firstChild.nodeValue;
1758
+ }
1759
+ response = eval("(" + innerHTML + ")");
1760
+ } catch(error){
1761
+ this.log('Error when attempting to parse form upload response (' + error + ")", 'error');
1762
+ response = {success: false};
1763
+ }
1764
+
1765
+ return response;
1766
+ },
1767
+ /**
1768
+ * Creates iframe with unique name
1769
+ */
1770
+ _createIframe: function(id){
1771
+ // We can't use following code as the name attribute
1772
+ // won't be properly registered in IE6, and new window
1773
+ // on form submit will open
1774
+ // var iframe = document.createElement('iframe');
1775
+ // iframe.setAttribute('name', id);
1776
+
1777
+ var iframe = qq.toElement('<iframe src="javascript:false;" name="' + id + '" />');
1778
+ // src="javascript:false;" removes ie6 prompt on https
1779
+
1780
+ iframe.setAttribute('id', id);
1781
+
1782
+ iframe.style.display = 'none';
1783
+ document.body.appendChild(iframe);
1784
+
1785
+ return iframe;
1786
+ },
1787
+ /**
1788
+ * Creates form, that will be submitted to iframe
1789
+ */
1790
+ _createForm: function(iframe, params){
1791
+ // We can't use the following code in IE6
1792
+ // var form = document.createElement('form');
1793
+ // form.setAttribute('method', 'post');
1794
+ // form.setAttribute('enctype', 'multipart/form-data');
1795
+ // Because in this case file won't be attached to request
1796
+ var protocol = this._options.demoMode ? "GET" : "POST"
1797
+ var form = qq.toElement('<form method="' + protocol + '" enctype="multipart/form-data"></form>');
1798
+
1799
+ var queryString = qq.obj2url(params, this._options.endpoint);
1800
+
1801
+ form.setAttribute('action', queryString);
1802
+ form.setAttribute('target', iframe.name);
1803
+ form.style.display = 'none';
1804
+ document.body.appendChild(form);
1805
+
1806
+ return form;
1807
+ }
1808
+ });
1809
+ /**
1810
+ * Class for uploading files using xhr
1811
+ * @inherits qq.UploadHandlerAbstract
1812
+ */
1813
+ qq.UploadHandlerXhr = function(o){
1814
+ qq.UploadHandlerAbstract.apply(this, arguments);
1815
+
1816
+ this._files = [];
1817
+ this._xhrs = [];
1818
+
1819
+ // current loaded size in bytes for each file
1820
+ this._loaded = [];
1821
+ };
1822
+
1823
+ // static method
1824
+ qq.UploadHandlerXhr.isSupported = function(){
1825
+ var input = document.createElement('input');
1826
+ input.type = 'file';
1827
+
1828
+ return (
1829
+ 'multiple' in input &&
1830
+ typeof File != "undefined" &&
1831
+ typeof FormData != "undefined" &&
1832
+ typeof (new XMLHttpRequest()).upload != "undefined" );
1833
+ };
1834
+
1835
+ // @inherits qq.UploadHandlerAbstract
1836
+ qq.extend(qq.UploadHandlerXhr.prototype, qq.UploadHandlerAbstract.prototype)
1837
+
1838
+ qq.extend(qq.UploadHandlerXhr.prototype, {
1839
+ /**
1840
+ * Adds file to the queue
1841
+ * Returns id to use with upload, cancel
1842
+ **/
1843
+ add: function(file){
1844
+ if (!(file instanceof File)){
1845
+ throw new Error('Passed obj in not a File (in qq.UploadHandlerXhr)');
1846
+ }
1847
+
1848
+ return this._files.push(file) - 1;
1849
+ },
1850
+ getName: function(id){
1851
+ var file = this._files[id];
1852
+ // fix missing name in Safari 4
1853
+ //NOTE: fixed missing name firefox 11.0a2 file.fileName is actually undefined
1854
+ return (file.fileName !== null && file.fileName !== undefined) ? file.fileName : file.name;
1855
+ },
1856
+ getSize: function(id){
1857
+ var file = this._files[id];
1858
+ return file.fileSize != null ? file.fileSize : file.size;
1859
+ },
1860
+ /**
1861
+ * Returns uploaded bytes for file identified by id
1862
+ */
1863
+ getLoaded: function(id){
1864
+ return this._loaded[id] || 0;
1865
+ },
1866
+ isValid: function(id) {
1867
+ return this._files[id] !== undefined;
1868
+ },
1869
+ reset: function() {
1870
+ qq.UploadHandlerAbstract.prototype.reset.apply(this, arguments);
1871
+ this._files = [];
1872
+ this._xhrs = [];
1873
+ this._loaded = [];
1874
+ },
1875
+ /**
1876
+ * Sends the file identified by id and additional query params to the server
1877
+ * @param {Object} params name-value string pairs
1878
+ */
1879
+ _upload: function(id, params){
1880
+ this._options.onUpload(id, this.getName(id), true);
1881
+
1882
+ var file = this._files[id],
1883
+ name = this.getName(id),
1884
+ size = this.getSize(id);
1885
+
1886
+ this._loaded[id] = 0;
1887
+
1888
+ var xhr = this._xhrs[id] = new XMLHttpRequest();
1889
+ var self = this;
1890
+
1891
+ xhr.upload.onprogress = function(e){
1892
+ if (e.lengthComputable){
1893
+ self._loaded[id] = e.loaded;
1894
+ self._options.onProgress(id, name, e.loaded, e.total);
1895
+ }
1896
+ };
1897
+
1898
+ xhr.onreadystatechange = function(){
1899
+ if (xhr.readyState == 4){
1900
+ self._onComplete(id, xhr);
1901
+ }
1902
+ };
1903
+
1904
+ // build query string
1905
+ params = params || {};
1906
+ params[this._options.inputName] = name;
1907
+ var queryString = qq.obj2url(params, this._options.endpoint);
1908
+
1909
+ var protocol = this._options.demoMode ? "GET" : "POST";
1910
+ xhr.open(protocol, queryString, true);
1911
+ xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
1912
+ xhr.setRequestHeader("X-File-Name", encodeURIComponent(name));
1913
+ xhr.setRequestHeader("Cache-Control", "no-cache");
1914
+ if (this._options.forceMultipart) {
1915
+ var formData = new FormData();
1916
+ formData.append(this._options.inputName, file);
1917
+ file = formData;
1918
+ } else {
1919
+ xhr.setRequestHeader("Content-Type", "application/octet-stream");
1920
+ //NOTE: return mime type in xhr works on chrome 16.0.9 firefox 11.0a2
1921
+ xhr.setRequestHeader("X-Mime-Type",file.type );
1922
+ }
1923
+ for (key in this._options.customHeaders){
1924
+ xhr.setRequestHeader(key, this._options.customHeaders[key]);
1925
+ };
1926
+
1927
+ this.log('Sending upload request for ' + id);
1928
+ xhr.send(file);
1929
+ },
1930
+ _onComplete: function(id, xhr){
1931
+ "use strict";
1932
+ // the request was aborted/cancelled
1933
+ if (!this._files[id]) { return; }
1934
+
1935
+ var name = this.getName(id);
1936
+ var size = this.getSize(id);
1937
+ var response; //the parsed JSON response from the server, or the empty object if parsing failed.
1938
+
1939
+ this._options.onProgress(id, name, size, size);
1940
+
1941
+ this.log("xhr - server response received for " + id);
1942
+ this.log("responseText = " + xhr.responseText);
1943
+
1944
+ try {
1945
+ if (typeof JSON.parse === "function") {
1946
+ response = JSON.parse(xhr.responseText);
1947
+ } else {
1948
+ response = eval("(" + xhr.responseText + ")");
1949
+ }
1950
+ } catch(error){
1951
+ this.log('Error when attempting to parse xhr response text (' + error + ')', 'error');
1952
+ response = {};
1953
+ }
1954
+
1955
+ if (xhr.status !== 200 || !response.success){
1956
+ if (this._options.onAutoRetry(id, name, response, xhr)) {
1957
+ return;
1958
+ }
1959
+ }
1960
+
1961
+ this._options.onComplete(id, name, response, xhr);
1962
+
1963
+ this._xhrs[id] = null;
1964
+ this._dequeue(id);
1965
+ },
1966
+ _cancel: function(id){
1967
+ this._options.onCancel(id, this.getName(id));
1968
+
1969
+ this._files[id] = null;
1970
+
1971
+ if (this._xhrs[id]){
1972
+ this._xhrs[id].abort();
1973
+ this._xhrs[id] = null;
1974
+ }
1975
+ }
1976
+ });