fineuploader-rails 3.0

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