fineuploader-rails 3.0

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