fine-uploader-rails 3.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
1
+ Copyright 2012 Zak Grant
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.
@@ -0,0 +1,30 @@
1
+ # Fine Uploader 3.2 for Rails
2
+
3
+ [Fineuploader](http://fineuploader.com/) is a Javascript plugin written by [Andrew Valums](http://github.com/valums/), and actively developed by [Ray Nicholus](http://lnkd.in/Nkhx2C). This plugin uses an XMLHttpRequest (AJAX) for uploading multiple files with a progress-bar in FF3.6+, Safari4+, Chrome and falls back to hidden-iframe-based upload in other browsers (namely IE), providing good user experience everywhere. It does not use Flash, jQuery, or any external libraries.
4
+
5
+ This gem integrates this fantastic plugin with Rails 3.1+ Asset Pipeline.
6
+
7
+ [Plugin documentation](https://github.com/valums/file-uploader/blob/master/readme.md)
8
+
9
+ ## Installing Gem
10
+
11
+ gem 'fine-uploader-rails', '~> 3.2'
12
+
13
+ ## Using the javascripts
14
+
15
+ Require fineuploader in your app/assets/application.js file
16
+
17
+ //= require fineuploader
18
+
19
+ Fineuploader with JQuery wrapper
20
+
21
+ //= require fineuploader.jquery
22
+
23
+ ## Using the stylesheet
24
+
25
+ Require the stylesheet file to app/assets/stylesheets/application.css
26
+
27
+ *= require fineuploader
28
+
29
+ ## Thanks
30
+ Thanks to [Andrew Valums](http://github.com/valums/) and [Ray Nicholus](http://lnkd.in/Nkhx2C) for this plugin.
@@ -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 "fine-uploader-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.2"
4
+ end
5
+ end
@@ -0,0 +1,3209 @@
1
+ /**
2
+ * http://github.com/Valums-File-Uploader/file-uploader
3
+ *
4
+ * Multiple file upload component with progress-bar, drag-and-drop, support for all modern browsers.
5
+ *
6
+ * Original version: 1.0 © 2010 Andrew Valums ( andrew(at)valums.com )
7
+ * Current Maintainer (2.0+): © 2012, Ray Nicholus ( fineuploader(at)garstasio.com )
8
+ *
9
+ * Licensed under MIT license, GNU GPL 2 or later, GNU LGPL 2 or later, see license.txt.
10
+ */
11
+ /*globals window, navigator, document, FormData, File, HTMLInputElement, XMLHttpRequest*/
12
+ var qq = function(element) {
13
+ "use strict";
14
+
15
+ return {
16
+ hide: function() {
17
+ element.style.display = 'none';
18
+ return this;
19
+ },
20
+
21
+ /** Returns the function which detaches attached event */
22
+ attach: function(type, fn) {
23
+ if (element.addEventListener){
24
+ element.addEventListener(type, fn, false);
25
+ } else if (element.attachEvent){
26
+ element.attachEvent('on' + type, fn);
27
+ }
28
+ return function() {
29
+ qq(element).detach(type, fn);
30
+ };
31
+ },
32
+
33
+ detach: function(type, fn) {
34
+ if (element.removeEventListener){
35
+ element.removeEventListener(type, fn, false);
36
+ } else if (element.attachEvent){
37
+ element.detachEvent('on' + type, fn);
38
+ }
39
+ return this;
40
+ },
41
+
42
+ contains: function(descendant) {
43
+ // compareposition returns false in this case
44
+ if (element === descendant) {
45
+ return true;
46
+ }
47
+
48
+ if (element.contains){
49
+ return element.contains(descendant);
50
+ } else {
51
+ /*jslint bitwise: true*/
52
+ return !!(descendant.compareDocumentPosition(element) & 8);
53
+ }
54
+ },
55
+
56
+ /**
57
+ * Insert this element before elementB.
58
+ */
59
+ insertBefore: function(elementB) {
60
+ elementB.parentNode.insertBefore(element, elementB);
61
+ return this;
62
+ },
63
+
64
+ remove: function() {
65
+ element.parentNode.removeChild(element);
66
+ return this;
67
+ },
68
+
69
+ /**
70
+ * Sets styles for an element.
71
+ * Fixes opacity in IE6-8.
72
+ */
73
+ css: function(styles) {
74
+ if (styles.opacity !== null){
75
+ if (typeof element.style.opacity !== 'string' && typeof(element.filters) !== 'undefined'){
76
+ styles.filter = 'alpha(opacity=' + Math.round(100 * styles.opacity) + ')';
77
+ }
78
+ }
79
+ qq.extend(element.style, styles);
80
+
81
+ return this;
82
+ },
83
+
84
+ hasClass: function(name) {
85
+ var re = new RegExp('(^| )' + name + '( |$)');
86
+ return re.test(element.className);
87
+ },
88
+
89
+ addClass: function(name) {
90
+ if (!qq(element).hasClass(name)){
91
+ element.className += ' ' + name;
92
+ }
93
+ return this;
94
+ },
95
+
96
+ removeClass: function(name) {
97
+ var re = new RegExp('(^| )' + name + '( |$)');
98
+ element.className = element.className.replace(re, ' ').replace(/^\s+|\s+$/g, "");
99
+ return this;
100
+ },
101
+
102
+ getByClass: function(className) {
103
+ var candidates,
104
+ result = [];
105
+
106
+ if (element.querySelectorAll){
107
+ return element.querySelectorAll('.' + className);
108
+ }
109
+
110
+ candidates = element.getElementsByTagName("*");
111
+
112
+ qq.each(candidates, function(idx, val) {
113
+ if (qq(val).hasClass(className)){
114
+ result.push(val);
115
+ }
116
+ });
117
+ return result;
118
+ },
119
+
120
+ children: function() {
121
+ var children = [],
122
+ child = element.firstChild;
123
+
124
+ while (child){
125
+ if (child.nodeType === 1){
126
+ children.push(child);
127
+ }
128
+ child = child.nextSibling;
129
+ }
130
+
131
+ return children;
132
+ },
133
+
134
+ setText: function(text) {
135
+ element.innerText = text;
136
+ element.textContent = text;
137
+ return this;
138
+ },
139
+
140
+ clearText: function() {
141
+ return qq(element).setText("");
142
+ }
143
+ };
144
+ };
145
+
146
+ qq.log = function(message, level) {
147
+ "use strict";
148
+
149
+ if (window.console) {
150
+ if (!level || level === 'info') {
151
+ window.console.log(message);
152
+ }
153
+ else
154
+ {
155
+ if (window.console[level]) {
156
+ window.console[level](message);
157
+ }
158
+ else {
159
+ window.console.log('<' + level + '> ' + message);
160
+ }
161
+ }
162
+ }
163
+ };
164
+
165
+ qq.isObject = function(variable) {
166
+ "use strict";
167
+ return variable !== null && variable && typeof(variable) === "object" && variable.constructor === Object;
168
+ };
169
+
170
+ qq.isFunction = function(variable) {
171
+ "use strict";
172
+ return typeof(variable) === "function";
173
+ };
174
+
175
+ qq.isFileOrInput = function(maybeFileOrInput) {
176
+ "use strict";
177
+ if (window.File && maybeFileOrInput instanceof File) {
178
+ return true;
179
+ }
180
+ else if (window.HTMLInputElement) {
181
+ if (maybeFileOrInput instanceof HTMLInputElement) {
182
+ if (maybeFileOrInput.type && maybeFileOrInput.type.toLowerCase() === 'file') {
183
+ return true;
184
+ }
185
+ }
186
+ }
187
+ else if (maybeFileOrInput.tagName) {
188
+ if (maybeFileOrInput.tagName.toLowerCase() === 'input') {
189
+ if (maybeFileOrInput.type && maybeFileOrInput.type.toLowerCase() === 'file') {
190
+ return true;
191
+ }
192
+ }
193
+ }
194
+
195
+ return false;
196
+ };
197
+
198
+ qq.isXhrUploadSupported = function() {
199
+ "use strict";
200
+ var input = document.createElement('input');
201
+ input.type = 'file';
202
+
203
+ return (
204
+ input.multiple !== undefined &&
205
+ typeof File !== "undefined" &&
206
+ typeof FormData !== "undefined" &&
207
+ typeof (new XMLHttpRequest()).upload !== "undefined" );
208
+ };
209
+
210
+ qq.isFolderDropSupported = function(dataTransfer) {
211
+ "use strict";
212
+ return (dataTransfer.items && dataTransfer.items[0].webkitGetAsEntry);
213
+ };
214
+
215
+ qq.isFileChunkingSupported = function() {
216
+ "use strict";
217
+ return !qq.android() && //android's impl of Blob.slice is broken
218
+ qq.isXhrUploadSupported() &&
219
+ (File.prototype.slice || File.prototype.webkitSlice || File.prototype.mozSlice);
220
+ };
221
+
222
+ qq.extend = function (first, second, extendNested) {
223
+ "use strict";
224
+ qq.each(second, function(prop, val) {
225
+ if (extendNested && qq.isObject(val)) {
226
+ if (first[prop] === undefined) {
227
+ first[prop] = {};
228
+ }
229
+ qq.extend(first[prop], val, true);
230
+ }
231
+ else {
232
+ first[prop] = val;
233
+ }
234
+ });
235
+ };
236
+
237
+ /**
238
+ * Searches for a given element in the array, returns -1 if it is not present.
239
+ * @param {Number} [from] The index at which to begin the search
240
+ */
241
+ qq.indexOf = function(arr, elt, from){
242
+ "use strict";
243
+
244
+ if (arr.indexOf) {
245
+ return arr.indexOf(elt, from);
246
+ }
247
+
248
+ from = from || 0;
249
+ var len = arr.length;
250
+
251
+ if (from < 0) {
252
+ from += len;
253
+ }
254
+
255
+ for (; from < len; from+=1){
256
+ if (arr.hasOwnProperty(from) && arr[from] === elt){
257
+ return from;
258
+ }
259
+ }
260
+ return -1;
261
+ };
262
+
263
+ //this is a version 4 UUID
264
+ qq.getUniqueId = function(){
265
+ "use strict";
266
+
267
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
268
+ /*jslint eqeq: true, bitwise: true*/
269
+ var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
270
+ return v.toString(16);
271
+ });
272
+ };
273
+
274
+ //
275
+ // Browsers and platforms detection
276
+
277
+ qq.ie = function(){
278
+ "use strict";
279
+ return navigator.userAgent.indexOf('MSIE') !== -1;
280
+ };
281
+ qq.ie10 = function(){
282
+ "use strict";
283
+ return navigator.userAgent.indexOf('MSIE 10') !== -1;
284
+ };
285
+ qq.safari = function(){
286
+ "use strict";
287
+ return navigator.vendor !== undefined && navigator.vendor.indexOf("Apple") !== -1;
288
+ };
289
+ qq.chrome = function(){
290
+ "use strict";
291
+ return navigator.vendor !== undefined && navigator.vendor.indexOf('Google') !== -1;
292
+ };
293
+ qq.firefox = function(){
294
+ "use strict";
295
+ return (navigator.userAgent.indexOf('Mozilla') !== -1 && navigator.vendor !== undefined && navigator.vendor === '');
296
+ };
297
+ qq.windows = function(){
298
+ "use strict";
299
+ return navigator.platform === "Win32";
300
+ };
301
+ qq.android = function(){
302
+ "use strict";
303
+ return navigator.userAgent.toLowerCase().indexOf('android') !== -1;
304
+ };
305
+
306
+ //
307
+ // Events
308
+
309
+ qq.preventDefault = function(e){
310
+ "use strict";
311
+ if (e.preventDefault){
312
+ e.preventDefault();
313
+ } else{
314
+ e.returnValue = false;
315
+ }
316
+ };
317
+
318
+ /**
319
+ * Creates and returns element from html string
320
+ * Uses innerHTML to create an element
321
+ */
322
+ qq.toElement = (function(){
323
+ "use strict";
324
+ var div = document.createElement('div');
325
+ return function(html){
326
+ div.innerHTML = html;
327
+ var element = div.firstChild;
328
+ div.removeChild(element);
329
+ return element;
330
+ };
331
+ }());
332
+
333
+ //key and value are passed to callback for each item in the object or array
334
+ qq.each = function(obj, callback) {
335
+ "use strict";
336
+ var key, retVal;
337
+ if (obj) {
338
+ for (key in obj) {
339
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
340
+ retVal = callback(key, obj[key]);
341
+ if (retVal === false) {
342
+ break;
343
+ }
344
+ }
345
+ }
346
+ }
347
+ };
348
+
349
+ /**
350
+ * obj2url() takes a json-object as argument and generates
351
+ * a querystring. pretty much like jQuery.param()
352
+ *
353
+ * how to use:
354
+ *
355
+ * `qq.obj2url({a:'b',c:'d'},'http://any.url/upload?otherParam=value');`
356
+ *
357
+ * will result in:
358
+ *
359
+ * `http://any.url/upload?otherParam=value&a=b&c=d`
360
+ *
361
+ * @param Object JSON-Object
362
+ * @param String current querystring-part
363
+ * @return String encoded querystring
364
+ */
365
+ qq.obj2url = function(obj, temp, prefixDone){
366
+ "use strict";
367
+ /*jshint laxbreak: true*/
368
+ var i, len,
369
+ uristrings = [],
370
+ prefix = '&',
371
+ add = function(nextObj, i){
372
+ var nextTemp = temp
373
+ ? (/\[\]$/.test(temp)) // prevent double-encoding
374
+ ? temp
375
+ : temp+'['+i+']'
376
+ : i;
377
+ if ((nextTemp !== 'undefined') && (i !== 'undefined')) {
378
+ uristrings.push(
379
+ (typeof nextObj === 'object')
380
+ ? qq.obj2url(nextObj, nextTemp, true)
381
+ : (Object.prototype.toString.call(nextObj) === '[object Function]')
382
+ ? encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj())
383
+ : encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj)
384
+ );
385
+ }
386
+ };
387
+
388
+ if (!prefixDone && temp) {
389
+ prefix = (/\?/.test(temp)) ? (/\?$/.test(temp)) ? '' : '&' : '?';
390
+ uristrings.push(temp);
391
+ uristrings.push(qq.obj2url(obj));
392
+ } else if ((Object.prototype.toString.call(obj) === '[object Array]') && (typeof obj !== 'undefined') ) {
393
+ // we wont use a for-in-loop on an array (performance)
394
+ for (i = -1, len = obj.length; i < len; i+=1){
395
+ add(obj[i], i);
396
+ }
397
+ } else if ((typeof obj !== 'undefined') && (obj !== null) && (typeof obj === "object")){
398
+ // for anything else but a scalar, we will use for-in-loop
399
+ for (i in obj){
400
+ if (obj.hasOwnProperty(i)) {
401
+ add(obj[i], i);
402
+ }
403
+ }
404
+ } else {
405
+ uristrings.push(encodeURIComponent(temp) + '=' + encodeURIComponent(obj));
406
+ }
407
+
408
+ if (temp) {
409
+ return uristrings.join(prefix);
410
+ } else {
411
+ return uristrings.join(prefix)
412
+ .replace(/^&/, '')
413
+ .replace(/%20/g, '+');
414
+ }
415
+ };
416
+
417
+ qq.obj2FormData = function(obj, formData, arrayKeyName) {
418
+ "use strict";
419
+ if (!formData) {
420
+ formData = new FormData();
421
+ }
422
+
423
+ qq.each(obj, function(key, val) {
424
+ key = arrayKeyName ? arrayKeyName + '[' + key + ']' : key;
425
+
426
+ if (qq.isObject(val)) {
427
+ qq.obj2FormData(val, formData, key);
428
+ }
429
+ else if (qq.isFunction(val)) {
430
+ formData.append(encodeURIComponent(key), encodeURIComponent(val()));
431
+ }
432
+ else {
433
+ formData.append(encodeURIComponent(key), encodeURIComponent(val));
434
+ }
435
+ });
436
+
437
+ return formData;
438
+ };
439
+
440
+ qq.obj2Inputs = function(obj, form) {
441
+ "use strict";
442
+ var input;
443
+
444
+ if (!form) {
445
+ form = document.createElement('form');
446
+ }
447
+
448
+ qq.obj2FormData(obj, {
449
+ append: function(key, val) {
450
+ input = document.createElement('input');
451
+ input.setAttribute('name', key);
452
+ input.setAttribute('value', val);
453
+ form.appendChild(input);
454
+ }
455
+ });
456
+
457
+ return form;
458
+ };
459
+
460
+ qq.setCookie = function(name, value, days) {
461
+ var date = new Date(),
462
+ expires = "";
463
+
464
+ if (days) {
465
+ date.setTime(date.getTime()+(days*24*60*60*1000));
466
+ expires = "; expires="+date.toGMTString();
467
+ }
468
+
469
+ document.cookie = name+"="+value+expires+"; path=/";
470
+ };
471
+
472
+ qq.getCookie = function(name) {
473
+ var nameEQ = name + "=",
474
+ ca = document.cookie.split(';'),
475
+ c;
476
+
477
+ for(var i=0;i < ca.length;i++) {
478
+ c = ca[i];
479
+ while (c.charAt(0)==' ') {
480
+ c = c.substring(1,c.length);
481
+ }
482
+ if (c.indexOf(nameEQ) === 0) {
483
+ return c.substring(nameEQ.length,c.length);
484
+ }
485
+ }
486
+ };
487
+
488
+ qq.getCookieNames = function(regexp) {
489
+ var cookies = document.cookie.split(';'),
490
+ cookieNames = [];
491
+
492
+ qq.each(cookies, function(idx, cookie) {
493
+ cookie = cookie.trim();
494
+
495
+ var equalsIdx = cookie.indexOf("=");
496
+
497
+ if (cookie.match(regexp)) {
498
+ cookieNames.push(cookie.substr(0, equalsIdx));
499
+ }
500
+ });
501
+
502
+ return cookieNames;
503
+ };
504
+
505
+ qq.deleteCookie = function(name) {
506
+ qq.setCookie(name, "", -1);
507
+ };
508
+
509
+ qq.areCookiesEnabled = function() {
510
+ var randNum = Math.random() * 100000,
511
+ name = "qqCookieTest:" + randNum;
512
+ qq.setCookie(name, 1);
513
+
514
+ if (qq.getCookie(name)) {
515
+ qq.deleteCookie(name);
516
+ return true;
517
+ }
518
+ return false;
519
+ };
520
+
521
+ /**
522
+ * Not recommended for use outside of Fine Uploader since this falls back to an unchecked eval if JSON.parse is not
523
+ * implemented. For a more secure JSON.parse polyfill, use Douglas Crockford's json2.js.
524
+ */
525
+ qq.parseJson = function(json) {
526
+ /*jshint evil: true*/
527
+ if (typeof JSON.parse === "function") {
528
+ return JSON.parse(json);
529
+ } else {
530
+ return eval("(" + json + ")");
531
+ }
532
+ };
533
+
534
+ /**
535
+ * A generic module which supports object disposing in dispose() method.
536
+ * */
537
+ qq.DisposeSupport = function() {
538
+ "use strict";
539
+ var disposers = [];
540
+
541
+ return {
542
+ /** Run all registered disposers */
543
+ dispose: function() {
544
+ var disposer;
545
+ do {
546
+ disposer = disposers.shift();
547
+ if (disposer) {
548
+ disposer();
549
+ }
550
+ }
551
+ while (disposer);
552
+ },
553
+
554
+ /** Attach event handler and register de-attacher as a disposer */
555
+ attach: function() {
556
+ var args = arguments;
557
+ /*jslint undef:true*/
558
+ this.addDisposer(qq(args[0]).attach.apply(this, Array.prototype.slice.call(arguments, 1)));
559
+ },
560
+
561
+ /** Add disposer to the collection */
562
+ addDisposer: function(disposeFunction) {
563
+ disposers.push(disposeFunction);
564
+ }
565
+ };
566
+ };
567
+ qq.UploadButton = function(o){
568
+ this._options = {
569
+ element: null,
570
+ // if set to true adds multiple attribute to file input
571
+ multiple: false,
572
+ acceptFiles: null,
573
+ // name attribute of file input
574
+ name: 'file',
575
+ onChange: function(input){},
576
+ hoverClass: 'qq-upload-button-hover',
577
+ focusClass: 'qq-upload-button-focus'
578
+ };
579
+
580
+ qq.extend(this._options, o);
581
+ this._disposeSupport = new qq.DisposeSupport();
582
+
583
+ this._element = this._options.element;
584
+
585
+ // make button suitable container for input
586
+ qq(this._element).css({
587
+ position: 'relative',
588
+ overflow: 'hidden',
589
+ // Make sure browse button is in the right side
590
+ // in Internet Explorer
591
+ direction: 'ltr'
592
+ });
593
+
594
+ this._input = this._createInput();
595
+ };
596
+
597
+ qq.UploadButton.prototype = {
598
+ /* returns file input element */
599
+ getInput: function(){
600
+ return this._input;
601
+ },
602
+ /* cleans/recreates the file input */
603
+ reset: function(){
604
+ if (this._input.parentNode){
605
+ qq(this._input).remove();
606
+ }
607
+
608
+ qq(this._element).removeClass(this._options.focusClass);
609
+ this._input = this._createInput();
610
+ },
611
+ _createInput: function(){
612
+ var input = document.createElement("input");
613
+
614
+ if (this._options.multiple){
615
+ input.setAttribute("multiple", "multiple");
616
+ }
617
+
618
+ if (this._options.acceptFiles) input.setAttribute("accept", this._options.acceptFiles);
619
+
620
+ input.setAttribute("type", "file");
621
+ input.setAttribute("name", this._options.name);
622
+
623
+ qq(input).css({
624
+ position: 'absolute',
625
+ // in Opera only 'browse' button
626
+ // is clickable and it is located at
627
+ // the right side of the input
628
+ right: 0,
629
+ top: 0,
630
+ fontFamily: 'Arial',
631
+ // 4 persons reported this, the max values that worked for them were 243, 236, 236, 118
632
+ fontSize: '118px',
633
+ margin: 0,
634
+ padding: 0,
635
+ cursor: 'pointer',
636
+ opacity: 0
637
+ });
638
+
639
+ this._element.appendChild(input);
640
+
641
+ var self = this;
642
+ this._disposeSupport.attach(input, 'change', function(){
643
+ self._options.onChange(input);
644
+ });
645
+
646
+ this._disposeSupport.attach(input, 'mouseover', function(){
647
+ qq(self._element).addClass(self._options.hoverClass);
648
+ });
649
+ this._disposeSupport.attach(input, 'mouseout', function(){
650
+ qq(self._element).removeClass(self._options.hoverClass);
651
+ });
652
+ this._disposeSupport.attach(input, 'focus', function(){
653
+ qq(self._element).addClass(self._options.focusClass);
654
+ });
655
+ this._disposeSupport.attach(input, 'blur', function(){
656
+ qq(self._element).removeClass(self._options.focusClass);
657
+ });
658
+
659
+ // IE and Opera, unfortunately have 2 tab stops on file input
660
+ // which is unacceptable in our case, disable keyboard access
661
+ if (window.attachEvent){
662
+ // it is IE or Opera
663
+ input.setAttribute('tabIndex', "-1");
664
+ }
665
+
666
+ return input;
667
+ }
668
+ };
669
+ qq.FineUploaderBasic = function(o){
670
+ var that = this;
671
+ this._options = {
672
+ debug: false,
673
+ button: null,
674
+ multiple: true,
675
+ maxConnections: 3,
676
+ disableCancelForFormUploads: false,
677
+ autoUpload: true,
678
+ request: {
679
+ endpoint: '/server/upload',
680
+ params: {},
681
+ paramsInBody: false,
682
+ customHeaders: {},
683
+ forceMultipart: true,
684
+ inputName: 'qqfile',
685
+ uuidName: 'qquuid',
686
+ totalFileSizeName: 'qqtotalfilesize'
687
+ },
688
+ validation: {
689
+ allowedExtensions: [],
690
+ sizeLimit: 0,
691
+ minSizeLimit: 0,
692
+ stopOnFirstInvalidFile: true
693
+ },
694
+ callbacks: {
695
+ onSubmit: function(id, fileName){},
696
+ onComplete: function(id, fileName, responseJSON){},
697
+ onCancel: function(id, fileName){},
698
+ onUpload: function(id, fileName){},
699
+ onUploadChunk: function(id, fileName, chunkData){},
700
+ onResume: function(id, fileName, chunkData){},
701
+ onProgress: function(id, fileName, loaded, total){},
702
+ onError: function(id, fileName, reason) {},
703
+ onAutoRetry: function(id, fileName, attemptNumber) {},
704
+ onManualRetry: function(id, fileName) {},
705
+ onValidateBatch: function(fileData) {},
706
+ onValidate: function(fileData) {}
707
+ },
708
+ messages: {
709
+ typeError: "{file} has an invalid extension. Valid extension(s): {extensions}.",
710
+ sizeError: "{file} is too large, maximum file size is {sizeLimit}.",
711
+ minSizeError: "{file} is too small, minimum file size is {minSizeLimit}.",
712
+ emptyError: "{file} is empty, please select files again without it.",
713
+ noFilesError: "No files to upload.",
714
+ onLeave: "The files are being uploaded, if you leave now the upload will be cancelled."
715
+ },
716
+ retry: {
717
+ enableAuto: false,
718
+ maxAutoAttempts: 3,
719
+ autoAttemptDelay: 5,
720
+ preventRetryResponseProperty: 'preventRetry'
721
+ },
722
+ classes: {
723
+ buttonHover: 'qq-upload-button-hover',
724
+ buttonFocus: 'qq-upload-button-focus'
725
+ },
726
+ chunking: {
727
+ enabled: false,
728
+ partSize: 2000000,
729
+ paramNames: {
730
+ partIndex: 'qqpartindex',
731
+ partByteOffset: 'qqpartbyteoffset',
732
+ chunkSize: 'qqchunksize',
733
+ totalFileSize: 'qqtotalfilesize',
734
+ totalParts: 'qqtotalparts',
735
+ filename: 'qqfilename'
736
+ }
737
+ },
738
+ resume: {
739
+ enabled: false,
740
+ id: null,
741
+ cookiesExpireIn: 7, //days
742
+ paramNames: {
743
+ resuming: "qqresume"
744
+ }
745
+ },
746
+ formatFileName: function(fileName) {
747
+ if (fileName.length > 33) {
748
+ fileName = fileName.slice(0, 19) + '...' + fileName.slice(-14);
749
+ }
750
+ return fileName;
751
+ },
752
+ text: {
753
+ sizeSymbols: ['kB', 'MB', 'GB', 'TB', 'PB', 'EB']
754
+ }
755
+ };
756
+
757
+ qq.extend(this._options, o, true);
758
+ this._wrapCallbacks();
759
+ this._disposeSupport = new qq.DisposeSupport();
760
+
761
+ // number of files being uploaded
762
+ this._filesInProgress = [];
763
+
764
+ this._storedFileIds = [];
765
+
766
+ this._autoRetries = [];
767
+ this._retryTimeouts = [];
768
+ this._preventRetries = [];
769
+
770
+ this._paramsStore = this._createParamsStore();
771
+ this._endpointStore = this._createEndpointStore();
772
+
773
+ this._handler = this._createUploadHandler();
774
+
775
+ if (this._options.button){
776
+ this._button = this._createUploadButton(this._options.button);
777
+ }
778
+
779
+ this._preventLeaveInProgress();
780
+ };
781
+
782
+ qq.FineUploaderBasic.prototype = {
783
+ log: function(str, level) {
784
+ if (this._options.debug && (!level || level === 'info')) {
785
+ qq.log('[FineUploader] ' + str);
786
+ }
787
+ else if (level && level !== 'info') {
788
+ qq.log('[FineUploader] ' + str, level);
789
+
790
+ }
791
+ },
792
+ setParams: function(params, fileId) {
793
+ /*jshint eqeqeq: true, eqnull: true*/
794
+ if (fileId == null) {
795
+ this._options.request.params = params;
796
+ }
797
+ else {
798
+ this._paramsStore.setParams(params, fileId);
799
+ }
800
+ },
801
+ setEndpoint: function(endpoint, fileId) {
802
+ /*jshint eqeqeq: true, eqnull: true*/
803
+ if (fileId == null) {
804
+ this._options.request.endpoint = endpoint;
805
+ }
806
+ else {
807
+ this._endpointStore.setEndpoint(endpoint, fileId);
808
+ }
809
+ },
810
+ getInProgress: function(){
811
+ return this._filesInProgress.length;
812
+ },
813
+ uploadStoredFiles: function(){
814
+ "use strict";
815
+ var idToUpload;
816
+
817
+ while(this._storedFileIds.length) {
818
+ idToUpload = this._storedFileIds.shift();
819
+ this._filesInProgress.push(idToUpload);
820
+ this._handler.upload(idToUpload);
821
+ }
822
+ },
823
+ clearStoredFiles: function(){
824
+ this._storedFileIds = [];
825
+ },
826
+ retry: function(id) {
827
+ if (this._onBeforeManualRetry(id)) {
828
+ this._handler.retry(id);
829
+ return true;
830
+ }
831
+ else {
832
+ return false;
833
+ }
834
+ },
835
+ cancel: function(fileId) {
836
+ this._handler.cancel(fileId);
837
+ },
838
+ reset: function() {
839
+ this.log("Resetting uploader...");
840
+ this._handler.reset();
841
+ this._filesInProgress = [];
842
+ this._storedFileIds = [];
843
+ this._autoRetries = [];
844
+ this._retryTimeouts = [];
845
+ this._preventRetries = [];
846
+ this._button.reset();
847
+ this._paramsStore.reset();
848
+ this._endpointStore.reset();
849
+ },
850
+ addFiles: function(filesOrInputs) {
851
+ var self = this,
852
+ verifiedFilesOrInputs = [],
853
+ index, fileOrInput;
854
+
855
+ if (filesOrInputs) {
856
+ if (!window.FileList || !(filesOrInputs instanceof FileList)) {
857
+ filesOrInputs = [].concat(filesOrInputs);
858
+ }
859
+
860
+ for (index = 0; index < filesOrInputs.length; index+=1) {
861
+ fileOrInput = filesOrInputs[index];
862
+
863
+ if (qq.isFileOrInput(fileOrInput)) {
864
+ verifiedFilesOrInputs.push(fileOrInput);
865
+ }
866
+ else {
867
+ self.log(fileOrInput + ' is not a File or INPUT element! Ignoring!', 'warn');
868
+ }
869
+ }
870
+
871
+ this.log('Processing ' + verifiedFilesOrInputs.length + ' files or inputs...');
872
+ this._uploadFileList(verifiedFilesOrInputs);
873
+ }
874
+ },
875
+ getUuid: function(fileId) {
876
+ return this._handler.getUuid(fileId);
877
+ },
878
+ getResumableFilesData: function() {
879
+ return this._handler.getResumableFilesData();
880
+ },
881
+ getSize: function(fileId) {
882
+ return this._handler.getSize(fileId);
883
+ },
884
+ getFile: function(fileId) {
885
+ return this._handler.getFile(fileId);
886
+ },
887
+ _createUploadButton: function(element){
888
+ var self = this;
889
+
890
+ var button = new qq.UploadButton({
891
+ element: element,
892
+ multiple: this._options.multiple && qq.isXhrUploadSupported(),
893
+ acceptFiles: this._options.validation.acceptFiles,
894
+ onChange: function(input){
895
+ self._onInputChange(input);
896
+ },
897
+ hoverClass: this._options.classes.buttonHover,
898
+ focusClass: this._options.classes.buttonFocus
899
+ });
900
+
901
+ this._disposeSupport.addDisposer(function() { button.dispose(); });
902
+ return button;
903
+ },
904
+ _createUploadHandler: function(){
905
+ var self = this;
906
+
907
+ return new qq.UploadHandler({
908
+ debug: this._options.debug,
909
+ forceMultipart: this._options.request.forceMultipart,
910
+ maxConnections: this._options.maxConnections,
911
+ customHeaders: this._options.request.customHeaders,
912
+ inputName: this._options.request.inputName,
913
+ uuidParamName: this._options.request.uuidName,
914
+ totalFileSizeParamName: this._options.request.totalFileSizeName,
915
+ demoMode: this._options.demoMode,
916
+ paramsInBody: this._options.request.paramsInBody,
917
+ paramsStore: this._paramsStore,
918
+ endpointStore: this._endpointStore,
919
+ chunking: this._options.chunking,
920
+ resume: this._options.resume,
921
+ log: function(str, level) {
922
+ self.log(str, level);
923
+ },
924
+ onProgress: function(id, fileName, loaded, total){
925
+ self._onProgress(id, fileName, loaded, total);
926
+ self._options.callbacks.onProgress(id, fileName, loaded, total);
927
+ },
928
+ onComplete: function(id, fileName, result, xhr){
929
+ self._onComplete(id, fileName, result, xhr);
930
+ self._options.callbacks.onComplete(id, fileName, result);
931
+ },
932
+ onCancel: function(id, fileName){
933
+ self._onCancel(id, fileName);
934
+ self._options.callbacks.onCancel(id, fileName);
935
+ },
936
+ onUpload: function(id, fileName){
937
+ self._onUpload(id, fileName);
938
+ self._options.callbacks.onUpload(id, fileName);
939
+ },
940
+ onUploadChunk: function(id, fileName, chunkData){
941
+ self._options.callbacks.onUploadChunk(id, fileName, chunkData);
942
+ },
943
+ onResume: function(id, fileName, chunkData) {
944
+ return self._options.callbacks.onResume(id, fileName, chunkData);
945
+ },
946
+ onAutoRetry: function(id, fileName, responseJSON, xhr) {
947
+ self._preventRetries[id] = responseJSON[self._options.retry.preventRetryResponseProperty];
948
+
949
+ if (self._shouldAutoRetry(id, fileName, responseJSON)) {
950
+ self._maybeParseAndSendUploadError(id, fileName, responseJSON, xhr);
951
+ self._options.callbacks.onAutoRetry(id, fileName, self._autoRetries[id] + 1);
952
+ self._onBeforeAutoRetry(id, fileName);
953
+
954
+ self._retryTimeouts[id] = setTimeout(function() {
955
+ self._onAutoRetry(id, fileName, responseJSON)
956
+ }, self._options.retry.autoAttemptDelay * 1000);
957
+
958
+ return true;
959
+ }
960
+ else {
961
+ return false;
962
+ }
963
+ }
964
+ });
965
+ },
966
+ _preventLeaveInProgress: function(){
967
+ var self = this;
968
+
969
+ this._disposeSupport.attach(window, 'beforeunload', function(e){
970
+ if (!self._filesInProgress.length){return;}
971
+
972
+ var e = e || window.event;
973
+ // for ie, ff
974
+ e.returnValue = self._options.messages.onLeave;
975
+ // for webkit
976
+ return self._options.messages.onLeave;
977
+ });
978
+ },
979
+ _onSubmit: function(id, fileName){
980
+ if (this._options.autoUpload) {
981
+ this._filesInProgress.push(id);
982
+ }
983
+ },
984
+ _onProgress: function(id, fileName, loaded, total){
985
+ },
986
+ _onComplete: function(id, fileName, result, xhr){
987
+ this._removeFromFilesInProgress(id);
988
+ this._maybeParseAndSendUploadError(id, fileName, result, xhr);
989
+ },
990
+ _onCancel: function(id, fileName){
991
+ this._removeFromFilesInProgress(id);
992
+
993
+ clearTimeout(this._retryTimeouts[id]);
994
+
995
+ var storedFileIndex = qq.indexOf(this._storedFileIds, id);
996
+ if (!this._options.autoUpload && storedFileIndex >= 0) {
997
+ this._storedFileIds.splice(storedFileIndex, 1);
998
+ }
999
+ },
1000
+ _removeFromFilesInProgress: function(id) {
1001
+ var index = qq.indexOf(this._filesInProgress, id);
1002
+ if (index >= 0) {
1003
+ this._filesInProgress.splice(index, 1);
1004
+ }
1005
+ },
1006
+ _onUpload: function(id, fileName){},
1007
+ _onInputChange: function(input){
1008
+ if (qq.isXhrUploadSupported()){
1009
+ this.addFiles(input.files);
1010
+ } else {
1011
+ this.addFiles(input);
1012
+ }
1013
+ this._button.reset();
1014
+ },
1015
+ _onBeforeAutoRetry: function(id, fileName) {
1016
+ this.log("Waiting " + this._options.retry.autoAttemptDelay + " seconds before retrying " + fileName + "...");
1017
+ },
1018
+ _onAutoRetry: function(id, fileName, responseJSON) {
1019
+ this.log("Retrying " + fileName + "...");
1020
+ this._autoRetries[id]++;
1021
+ this._handler.retry(id);
1022
+ },
1023
+ _shouldAutoRetry: function(id, fileName, responseJSON) {
1024
+ if (!this._preventRetries[id] && this._options.retry.enableAuto) {
1025
+ if (this._autoRetries[id] === undefined) {
1026
+ this._autoRetries[id] = 0;
1027
+ }
1028
+
1029
+ return this._autoRetries[id] < this._options.retry.maxAutoAttempts
1030
+ }
1031
+
1032
+ return false;
1033
+ },
1034
+ //return false if we should not attempt the requested retry
1035
+ _onBeforeManualRetry: function(id) {
1036
+ if (this._preventRetries[id]) {
1037
+ this.log("Retries are forbidden for id " + id, 'warn');
1038
+ return false;
1039
+ }
1040
+ else if (this._handler.isValid(id)) {
1041
+ var fileName = this._handler.getName(id);
1042
+
1043
+ if (this._options.callbacks.onManualRetry(id, fileName) === false) {
1044
+ return false;
1045
+ }
1046
+
1047
+ this.log("Retrying upload for '" + fileName + "' (id: " + id + ")...");
1048
+ this._filesInProgress.push(id);
1049
+ return true;
1050
+ }
1051
+ else {
1052
+ this.log("'" + id + "' is not a valid file ID", 'error');
1053
+ return false;
1054
+ }
1055
+ },
1056
+ _maybeParseAndSendUploadError: function(id, fileName, response, xhr) {
1057
+ //assuming no one will actually set the response code to something other than 200 and still set 'success' to true
1058
+ if (!response.success){
1059
+ if (xhr && xhr.status !== 200 && !response.error) {
1060
+ this._options.callbacks.onError(id, fileName, "XHR returned response code " + xhr.status);
1061
+ }
1062
+ else {
1063
+ var errorReason = response.error ? response.error : "Upload failure reason unknown";
1064
+ this._options.callbacks.onError(id, fileName, errorReason);
1065
+ }
1066
+ }
1067
+ },
1068
+ _uploadFileList: function(files){
1069
+ var validationDescriptors, index, batchInvalid;
1070
+
1071
+ validationDescriptors = this._getValidationDescriptors(files);
1072
+ batchInvalid = this._options.callbacks.onValidateBatch(validationDescriptors) === false;
1073
+
1074
+ if (!batchInvalid) {
1075
+ if (files.length > 0) {
1076
+ for (index = 0; index < files.length; index++){
1077
+ if (this._validateFile(files[index])){
1078
+ this._uploadFile(files[index]);
1079
+ } else {
1080
+ if (this._options.validation.stopOnFirstInvalidFile){
1081
+ return;
1082
+ }
1083
+ }
1084
+ }
1085
+ }
1086
+ else {
1087
+ this._error('noFilesError', "");
1088
+ }
1089
+ }
1090
+ },
1091
+ _uploadFile: function(fileContainer){
1092
+ var id = this._handler.add(fileContainer);
1093
+ var fileName = this._handler.getName(id);
1094
+
1095
+ if (this._options.callbacks.onSubmit(id, fileName) !== false){
1096
+ this._onSubmit(id, fileName);
1097
+ if (this._options.autoUpload) {
1098
+ this._handler.upload(id);
1099
+ }
1100
+ else {
1101
+ this._storeFileForLater(id);
1102
+ }
1103
+ }
1104
+ },
1105
+ _storeFileForLater: function(id) {
1106
+ this._storedFileIds.push(id);
1107
+ },
1108
+ _validateFile: function(file){
1109
+ var validationDescriptor, name, size;
1110
+
1111
+ validationDescriptor = this._getValidationDescriptor(file);
1112
+ name = validationDescriptor.name;
1113
+ size = validationDescriptor.size;
1114
+
1115
+ if (this._options.callbacks.onValidate(validationDescriptor) === false) {
1116
+ return false;
1117
+ }
1118
+
1119
+ if (!this._isAllowedExtension(name)){
1120
+ this._error('typeError', name);
1121
+ return false;
1122
+
1123
+ }
1124
+ else if (size === 0){
1125
+ this._error('emptyError', name);
1126
+ return false;
1127
+
1128
+ }
1129
+ else if (size && this._options.validation.sizeLimit && size > this._options.validation.sizeLimit){
1130
+ this._error('sizeError', name);
1131
+ return false;
1132
+
1133
+ }
1134
+ else if (size && size < this._options.validation.minSizeLimit){
1135
+ this._error('minSizeError', name);
1136
+ return false;
1137
+ }
1138
+
1139
+ return true;
1140
+ },
1141
+ _error: function(code, fileName){
1142
+ var message = this._options.messages[code];
1143
+ function r(name, replacement){ message = message.replace(name, replacement); }
1144
+
1145
+ var extensions = this._options.validation.allowedExtensions.join(', ').toLowerCase();
1146
+
1147
+ r('{file}', this._options.formatFileName(fileName));
1148
+ r('{extensions}', extensions);
1149
+ r('{sizeLimit}', this._formatSize(this._options.validation.sizeLimit));
1150
+ r('{minSizeLimit}', this._formatSize(this._options.validation.minSizeLimit));
1151
+
1152
+ this._options.callbacks.onError(null, fileName, message);
1153
+
1154
+ return message;
1155
+ },
1156
+ _isAllowedExtension: function(fileName){
1157
+ var allowed = this._options.validation.allowedExtensions,
1158
+ valid = false;
1159
+
1160
+ if (!allowed.length) {
1161
+ return true;
1162
+ }
1163
+
1164
+ qq.each(allowed, function(idx, allowedExt) {
1165
+ /*jshint eqeqeq: true, eqnull: true*/
1166
+ var extRegex = new RegExp('\\.' + allowedExt + "$", 'i');
1167
+
1168
+ if (fileName.match(extRegex) != null) {
1169
+ valid = true;
1170
+ return false;
1171
+ }
1172
+ });
1173
+
1174
+ return valid;
1175
+ },
1176
+ _formatSize: function(bytes){
1177
+ var i = -1;
1178
+ do {
1179
+ bytes = bytes / 1024;
1180
+ i++;
1181
+ } while (bytes > 99);
1182
+
1183
+ return Math.max(bytes, 0.1).toFixed(1) + this._options.text.sizeSymbols[i];
1184
+ },
1185
+ _wrapCallbacks: function() {
1186
+ var self, safeCallback;
1187
+
1188
+ self = this;
1189
+
1190
+ safeCallback = function(name, callback, args) {
1191
+ try {
1192
+ return callback.apply(self, args);
1193
+ }
1194
+ catch (exception) {
1195
+ self.log("Caught exception in '" + name + "' callback - " + exception.message, 'error');
1196
+ }
1197
+ }
1198
+
1199
+ for (var prop in this._options.callbacks) {
1200
+ (function() {
1201
+ var callbackName, callbackFunc;
1202
+ callbackName = prop;
1203
+ callbackFunc = self._options.callbacks[callbackName];
1204
+ self._options.callbacks[callbackName] = function() {
1205
+ return safeCallback(callbackName, callbackFunc, arguments);
1206
+ }
1207
+ }());
1208
+ }
1209
+ },
1210
+ _parseFileName: function(file) {
1211
+ var name;
1212
+
1213
+ if (file.value){
1214
+ // it is a file input
1215
+ // get input value and remove path to normalize
1216
+ name = file.value.replace(/.*(\/|\\)/, "");
1217
+ } else {
1218
+ // fix missing properties in Safari 4 and firefox 11.0a2
1219
+ name = (file.fileName !== null && file.fileName !== undefined) ? file.fileName : file.name;
1220
+ }
1221
+
1222
+ return name;
1223
+ },
1224
+ _parseFileSize: function(file) {
1225
+ var size;
1226
+
1227
+ if (!file.value){
1228
+ // fix missing properties in Safari 4 and firefox 11.0a2
1229
+ size = (file.fileSize !== null && file.fileSize !== undefined) ? file.fileSize : file.size;
1230
+ }
1231
+
1232
+ return size;
1233
+ },
1234
+ _getValidationDescriptor: function(file) {
1235
+ var name, size, fileDescriptor;
1236
+
1237
+ fileDescriptor = {};
1238
+ name = this._parseFileName(file);
1239
+ size = this._parseFileSize(file);
1240
+
1241
+ fileDescriptor.name = name;
1242
+ if (size) {
1243
+ fileDescriptor.size = size;
1244
+ }
1245
+
1246
+ return fileDescriptor;
1247
+ },
1248
+ _getValidationDescriptors: function(files) {
1249
+ var self = this,
1250
+ fileDescriptors = [];
1251
+
1252
+ qq.each(files, function(idx, file) {
1253
+ fileDescriptors.push(self._getValidationDescriptor(file));
1254
+ });
1255
+
1256
+ return fileDescriptors;
1257
+ },
1258
+ _createParamsStore: function() {
1259
+ var paramsStore = {},
1260
+ self = this;
1261
+
1262
+ return {
1263
+ setParams: function(params, fileId) {
1264
+ var paramsCopy = {};
1265
+ qq.extend(paramsCopy, params);
1266
+ paramsStore[fileId] = paramsCopy;
1267
+ },
1268
+
1269
+ getParams: function(fileId) {
1270
+ /*jshint eqeqeq: true, eqnull: true*/
1271
+ var paramsCopy = {};
1272
+
1273
+ if (fileId != null && paramsStore[fileId]) {
1274
+ qq.extend(paramsCopy, paramsStore[fileId]);
1275
+ }
1276
+ else {
1277
+ qq.extend(paramsCopy, self._options.request.params);
1278
+ }
1279
+
1280
+ return paramsCopy;
1281
+ },
1282
+
1283
+ remove: function(fileId) {
1284
+ return delete paramsStore[fileId];
1285
+ },
1286
+
1287
+ reset: function() {
1288
+ paramsStore = {};
1289
+ }
1290
+ };
1291
+ },
1292
+ _createEndpointStore: function() {
1293
+ var endpointStore = {},
1294
+ self = this;
1295
+
1296
+ return {
1297
+ setEndpoint: function(endpoint, fileId) {
1298
+ endpointStore[fileId] = endpoint;
1299
+ },
1300
+
1301
+ getEndpoint: function(fileId) {
1302
+ /*jshint eqeqeq: true, eqnull: true*/
1303
+ if (fileId != null && endpointStore[fileId]) {
1304
+ return endpointStore[fileId];
1305
+ }
1306
+
1307
+ return self._options.request.endpoint;
1308
+ },
1309
+
1310
+ remove: function(fileId) {
1311
+ return delete endpointStore[fileId];
1312
+ },
1313
+
1314
+ reset: function() {
1315
+ endpointStore = {};
1316
+ }
1317
+ };
1318
+ }
1319
+ };
1320
+ /*globals qq, document*/
1321
+ qq.DragAndDrop = function(o) {
1322
+ "use strict";
1323
+
1324
+ var options, dz, dirPending,
1325
+ droppedFiles = [],
1326
+ droppedEntriesCount = 0,
1327
+ droppedEntriesParsedCount = 0,
1328
+ disposeSupport = new qq.DisposeSupport();
1329
+
1330
+ options = {
1331
+ dropArea: null,
1332
+ extraDropzones: [],
1333
+ hideDropzones: true,
1334
+ multiple: true,
1335
+ classes: {
1336
+ dropActive: null
1337
+ },
1338
+ callbacks: {
1339
+ dropProcessing: function(isProcessing, files) {},
1340
+ error: function(code, filename) {},
1341
+ log: function(message, level) {}
1342
+ }
1343
+ };
1344
+
1345
+ qq.extend(options, o);
1346
+
1347
+ function maybeUploadDroppedFiles() {
1348
+ if (droppedEntriesCount === droppedEntriesParsedCount && !dirPending) {
1349
+ options.callbacks.log('Grabbed ' + droppedFiles.length + " files after tree traversal.");
1350
+ dz.dropDisabled(false);
1351
+ options.callbacks.dropProcessing(false, droppedFiles);
1352
+ }
1353
+ }
1354
+ function addDroppedFile(file) {
1355
+ droppedFiles.push(file);
1356
+ droppedEntriesParsedCount+=1;
1357
+ maybeUploadDroppedFiles();
1358
+ }
1359
+
1360
+ function traverseFileTree(entry) {
1361
+ var dirReader, i;
1362
+
1363
+ droppedEntriesCount+=1;
1364
+
1365
+ if (entry.isFile) {
1366
+ entry.file(function(file) {
1367
+ addDroppedFile(file);
1368
+ });
1369
+ }
1370
+ else if (entry.isDirectory) {
1371
+ dirPending = true;
1372
+ dirReader = entry.createReader();
1373
+ dirReader.readEntries(function(entries) {
1374
+ droppedEntriesParsedCount+=1;
1375
+ for (i = 0; i < entries.length; i+=1) {
1376
+ traverseFileTree(entries[i]);
1377
+ }
1378
+
1379
+ dirPending = false;
1380
+
1381
+ if (!entries.length) {
1382
+ maybeUploadDroppedFiles();
1383
+ }
1384
+ });
1385
+ }
1386
+ }
1387
+
1388
+ function handleDataTransfer(dataTransfer) {
1389
+ var i, items, entry;
1390
+
1391
+ options.callbacks.dropProcessing(true);
1392
+ dz.dropDisabled(true);
1393
+
1394
+ if (dataTransfer.files.length > 1 && !options.multiple) {
1395
+ options.callbacks.dropProcessing(false);
1396
+ options.callbacks.error('tooManyFilesError', "");
1397
+ dz.dropDisabled(false);
1398
+ }
1399
+ else {
1400
+ droppedFiles = [];
1401
+ droppedEntriesCount = 0;
1402
+ droppedEntriesParsedCount = 0;
1403
+
1404
+ if (qq.isFolderDropSupported(dataTransfer)) {
1405
+ items = dataTransfer.items;
1406
+
1407
+ for (i = 0; i < items.length; i+=1) {
1408
+ entry = items[i].webkitGetAsEntry();
1409
+ if (entry) {
1410
+ //due to a bug in Chrome's File System API impl - #149735
1411
+ if (entry.isFile) {
1412
+ droppedFiles.push(items[i].getAsFile());
1413
+ if (i === items.length-1) {
1414
+ maybeUploadDroppedFiles();
1415
+ }
1416
+ }
1417
+
1418
+ else {
1419
+ traverseFileTree(entry);
1420
+ }
1421
+ }
1422
+ }
1423
+ }
1424
+ else {
1425
+ options.callbacks.dropProcessing(false, dataTransfer.files);
1426
+ dz.dropDisabled(false);
1427
+ }
1428
+ }
1429
+ }
1430
+
1431
+ function setupDropzone(dropArea){
1432
+ dz = new qq.UploadDropZone({
1433
+ element: dropArea,
1434
+ onEnter: function(e){
1435
+ qq(dropArea).addClass(options.classes.dropActive);
1436
+ e.stopPropagation();
1437
+ },
1438
+ onLeaveNotDescendants: function(e){
1439
+ qq(dropArea).removeClass(options.classes.dropActive);
1440
+ },
1441
+ onDrop: function(e){
1442
+ if (options.hideDropzones) {
1443
+ qq(dropArea).hide();
1444
+ }
1445
+ qq(dropArea).removeClass(options.classes.dropActive);
1446
+
1447
+ handleDataTransfer(e.dataTransfer);
1448
+ }
1449
+ });
1450
+
1451
+ disposeSupport.addDisposer(function() {
1452
+ dz.dispose();
1453
+ });
1454
+
1455
+ if (options.hideDropzones) {
1456
+ qq(dropArea).hide();
1457
+ }
1458
+ }
1459
+
1460
+ function isFileDrag(dragEvent) {
1461
+ var fileDrag;
1462
+
1463
+ qq.each(dragEvent.dataTransfer.types, function(key, val) {
1464
+ if (val === 'Files') {
1465
+ fileDrag = true;
1466
+ return false;
1467
+ }
1468
+ });
1469
+
1470
+ return fileDrag;
1471
+ }
1472
+
1473
+ function setupDragDrop(){
1474
+ if (options.dropArea) {
1475
+ options.extraDropzones.push(options.dropArea);
1476
+ }
1477
+
1478
+ var i, dropzones = options.extraDropzones;
1479
+
1480
+ for (i=0; i < dropzones.length; i+=1){
1481
+ setupDropzone(dropzones[i]);
1482
+ }
1483
+
1484
+ // IE <= 9 does not support the File API used for drag+drop uploads
1485
+ if (options.dropArea && (!qq.ie() || qq.ie10())) {
1486
+ disposeSupport.attach(document, 'dragenter', function(e) {
1487
+ if (!dz.dropDisabled() && isFileDrag(e)) {
1488
+ if (qq(options.dropArea).hasClass(options.classes.dropDisabled)) {
1489
+ return;
1490
+ }
1491
+
1492
+ options.dropArea.style.display = 'block';
1493
+ for (i=0; i < dropzones.length; i+=1) {
1494
+ dropzones[i].style.display = 'block';
1495
+ }
1496
+ }
1497
+ });
1498
+ }
1499
+ disposeSupport.attach(document, 'dragleave', function(e){
1500
+ if (options.hideDropzones && qq.FineUploader.prototype._leaving_document_out(e)) {
1501
+ for (i=0; i < dropzones.length; i+=1) {
1502
+ qq(dropzones[i]).hide();
1503
+ }
1504
+ }
1505
+ });
1506
+ disposeSupport.attach(document, 'drop', function(e){
1507
+ if (options.hideDropzones) {
1508
+ for (i=0; i < dropzones.length; i+=1) {
1509
+ qq(dropzones[i]).hide();
1510
+ }
1511
+ }
1512
+ e.preventDefault();
1513
+ });
1514
+ }
1515
+
1516
+ return {
1517
+ setup: function() {
1518
+ setupDragDrop();
1519
+ },
1520
+
1521
+ setupExtraDropzone: function(element) {
1522
+ options.extraDropzones.push(element);
1523
+ setupDropzone(element);
1524
+ },
1525
+
1526
+ removeExtraDropzone: function(element) {
1527
+ var i, dzs = options.extraDropzones;
1528
+ for(i in dzs) {
1529
+ if (dzs[i] === element) {
1530
+ return dzs.splice(i, 1);
1531
+ }
1532
+ }
1533
+ },
1534
+
1535
+ dispose: function() {
1536
+ disposeSupport.dispose();
1537
+ dz.dispose();
1538
+ }
1539
+ };
1540
+ };
1541
+
1542
+
1543
+ qq.UploadDropZone = function(o){
1544
+ "use strict";
1545
+
1546
+ var options, element, preventDrop, dropOutsideDisabled, disposeSupport = new qq.DisposeSupport();
1547
+
1548
+ options = {
1549
+ element: null,
1550
+ onEnter: function(e){},
1551
+ onLeave: function(e){},
1552
+ // is not fired when leaving element by hovering descendants
1553
+ onLeaveNotDescendants: function(e){},
1554
+ onDrop: function(e){}
1555
+ };
1556
+
1557
+ qq.extend(options, o);
1558
+ element = options.element;
1559
+
1560
+ function dragover_should_be_canceled(){
1561
+ return qq.safari() || (qq.firefox() && qq.windows());
1562
+ }
1563
+
1564
+ function disableDropOutside(e){
1565
+ // run only once for all instances
1566
+ if (!dropOutsideDisabled ){
1567
+
1568
+ // for these cases we need to catch onDrop to reset dropArea
1569
+ if (dragover_should_be_canceled){
1570
+ disposeSupport.attach(document, 'dragover', function(e){
1571
+ e.preventDefault();
1572
+ });
1573
+ } else {
1574
+ disposeSupport.attach(document, 'dragover', function(e){
1575
+ if (e.dataTransfer){
1576
+ e.dataTransfer.dropEffect = 'none';
1577
+ e.preventDefault();
1578
+ }
1579
+ });
1580
+ }
1581
+
1582
+ dropOutsideDisabled = true;
1583
+ }
1584
+ }
1585
+
1586
+ function isValidFileDrag(e){
1587
+ // e.dataTransfer currently causing IE errors
1588
+ // IE9 does NOT support file API, so drag-and-drop is not possible
1589
+ if (qq.ie() && !qq.ie10()) {
1590
+ return false;
1591
+ }
1592
+
1593
+ var effectTest, dt = e.dataTransfer,
1594
+ // do not check dt.types.contains in webkit, because it crashes safari 4
1595
+ isSafari = qq.safari();
1596
+
1597
+ // dt.effectAllowed is none in Safari 5
1598
+ // dt.types.contains check is for firefox
1599
+ effectTest = qq.ie10() ? true : dt.effectAllowed !== 'none';
1600
+ return dt && effectTest && (dt.files || (!isSafari && dt.types.contains && dt.types.contains('Files')));
1601
+ }
1602
+
1603
+ function isOrSetDropDisabled(isDisabled) {
1604
+ if (isDisabled !== undefined) {
1605
+ preventDrop = isDisabled;
1606
+ }
1607
+ return preventDrop;
1608
+ }
1609
+
1610
+ function attachEvents(){
1611
+ disposeSupport.attach(element, 'dragover', function(e){
1612
+ if (!isValidFileDrag(e)) {
1613
+ return;
1614
+ }
1615
+
1616
+ var effect = qq.ie() ? null : e.dataTransfer.effectAllowed;
1617
+ if (effect === 'move' || effect === 'linkMove'){
1618
+ e.dataTransfer.dropEffect = 'move'; // for FF (only move allowed)
1619
+ } else {
1620
+ e.dataTransfer.dropEffect = 'copy'; // for Chrome
1621
+ }
1622
+
1623
+ e.stopPropagation();
1624
+ e.preventDefault();
1625
+ });
1626
+
1627
+ disposeSupport.attach(element, 'dragenter', function(e){
1628
+ if (!isOrSetDropDisabled()) {
1629
+ if (!isValidFileDrag(e)) {
1630
+ return;
1631
+ }
1632
+ options.onEnter(e);
1633
+ }
1634
+ });
1635
+
1636
+ disposeSupport.attach(element, 'dragleave', function(e){
1637
+ if (!isValidFileDrag(e)) {
1638
+ return;
1639
+ }
1640
+
1641
+ options.onLeave(e);
1642
+
1643
+ var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);
1644
+ // do not fire when moving a mouse over a descendant
1645
+ if (qq(this).contains(relatedTarget)) {
1646
+ return;
1647
+ }
1648
+
1649
+ options.onLeaveNotDescendants(e);
1650
+ });
1651
+
1652
+ disposeSupport.attach(element, 'drop', function(e){
1653
+ if (!isOrSetDropDisabled()) {
1654
+ if (!isValidFileDrag(e)) {
1655
+ return;
1656
+ }
1657
+
1658
+ e.preventDefault();
1659
+ options.onDrop(e);
1660
+ }
1661
+ });
1662
+ }
1663
+
1664
+ disableDropOutside();
1665
+ attachEvents();
1666
+
1667
+ return {
1668
+ dropDisabled: function(isDisabled) {
1669
+ return isOrSetDropDisabled(isDisabled);
1670
+ },
1671
+
1672
+ dispose: function() {
1673
+ disposeSupport.dispose();
1674
+ }
1675
+ };
1676
+ };
1677
+ /**
1678
+ * Class that creates upload widget with drag-and-drop and file list
1679
+ * @inherits qq.FineUploaderBasic
1680
+ */
1681
+ qq.FineUploader = function(o){
1682
+ // call parent constructor
1683
+ qq.FineUploaderBasic.apply(this, arguments);
1684
+
1685
+ // additional options
1686
+ qq.extend(this._options, {
1687
+ element: null,
1688
+ listElement: null,
1689
+ dragAndDrop: {
1690
+ extraDropzones: [],
1691
+ hideDropzones: true,
1692
+ disableDefaultDropzone: false
1693
+ },
1694
+ text: {
1695
+ uploadButton: 'Upload a file',
1696
+ cancelButton: 'Cancel',
1697
+ retryButton: 'Retry',
1698
+ failUpload: 'Upload failed',
1699
+ dragZone: 'Drop files here to upload',
1700
+ dropProcessing: 'Processing dropped files...',
1701
+ formatProgress: "{percent}% of {total_size}",
1702
+ waitingForResponse: "Processing..."
1703
+ },
1704
+ template: '<div class="qq-uploader">' +
1705
+ ((!this._options.dragAndDrop || !this._options.dragAndDrop.disableDefaultDropzone) ? '<div class="qq-upload-drop-area"><span>{dragZoneText}</span></div>' : '') +
1706
+ (!this._options.button ? '<div class="qq-upload-button"><div>{uploadButtonText}</div></div>' : '') +
1707
+ '<span class="qq-drop-processing"><span>{dropProcessingText}</span><span class="qq-drop-processing-spinner"></span></span>' +
1708
+ (!this._options.listElement ? '<ul class="qq-upload-list"></ul>' : '') +
1709
+ '</div>',
1710
+
1711
+ // template for one item in file list
1712
+ fileTemplate: '<li>' +
1713
+ '<div class="qq-progress-bar"></div>' +
1714
+ '<span class="qq-upload-spinner"></span>' +
1715
+ '<span class="qq-upload-finished"></span>' +
1716
+ '<span class="qq-upload-file"></span>' +
1717
+ '<span class="qq-upload-size"></span>' +
1718
+ '<a class="qq-upload-cancel" href="#">{cancelButtonText}</a>' +
1719
+ '<a class="qq-upload-retry" href="#">{retryButtonText}</a>' +
1720
+ '<span class="qq-upload-status-text">{statusText}</span>' +
1721
+ '</li>',
1722
+ classes: {
1723
+ button: 'qq-upload-button',
1724
+ drop: 'qq-upload-drop-area',
1725
+ dropActive: 'qq-upload-drop-area-active',
1726
+ dropDisabled: 'qq-upload-drop-area-disabled',
1727
+ list: 'qq-upload-list',
1728
+ progressBar: 'qq-progress-bar',
1729
+ file: 'qq-upload-file',
1730
+ spinner: 'qq-upload-spinner',
1731
+ finished: 'qq-upload-finished',
1732
+ retrying: 'qq-upload-retrying',
1733
+ retryable: 'qq-upload-retryable',
1734
+ size: 'qq-upload-size',
1735
+ cancel: 'qq-upload-cancel',
1736
+ retry: 'qq-upload-retry',
1737
+ statusText: 'qq-upload-status-text',
1738
+
1739
+ success: 'qq-upload-success',
1740
+ fail: 'qq-upload-fail',
1741
+
1742
+ successIcon: null,
1743
+ failIcon: null,
1744
+
1745
+ dropProcessing: 'qq-drop-processing',
1746
+ dropProcessingSpinner: 'qq-drop-processing-spinner'
1747
+ },
1748
+ failedUploadTextDisplay: {
1749
+ mode: 'default', //default, custom, or none
1750
+ maxChars: 50,
1751
+ responseProperty: 'error',
1752
+ enableTooltip: true
1753
+ },
1754
+ messages: {
1755
+ tooManyFilesError: "You may only drop one file"
1756
+ },
1757
+ retry: {
1758
+ showAutoRetryNote: true,
1759
+ autoRetryNote: "Retrying {retryNum}/{maxAuto}...",
1760
+ showButton: false
1761
+ },
1762
+ showMessage: function(message){
1763
+ setTimeout(function() {
1764
+ alert(message);
1765
+ }, 0);
1766
+ }
1767
+ }, true);
1768
+
1769
+ // overwrite options with user supplied
1770
+ qq.extend(this._options, o, true);
1771
+ this._wrapCallbacks();
1772
+
1773
+ // overwrite the upload button text if any
1774
+ // same for the Cancel button and Fail message text
1775
+ this._options.template = this._options.template.replace(/\{dragZoneText\}/g, this._options.text.dragZone);
1776
+ this._options.template = this._options.template.replace(/\{uploadButtonText\}/g, this._options.text.uploadButton);
1777
+ this._options.template = this._options.template.replace(/\{dropProcessingText\}/g, this._options.text.dropProcessing);
1778
+ this._options.fileTemplate = this._options.fileTemplate.replace(/\{cancelButtonText\}/g, this._options.text.cancelButton);
1779
+ this._options.fileTemplate = this._options.fileTemplate.replace(/\{retryButtonText\}/g, this._options.text.retryButton);
1780
+ this._options.fileTemplate = this._options.fileTemplate.replace(/\{statusText\}/g, "");
1781
+
1782
+ this._element = this._options.element;
1783
+ this._element.innerHTML = this._options.template;
1784
+ this._listElement = this._options.listElement || this._find(this._element, 'list');
1785
+
1786
+ this._classes = this._options.classes;
1787
+
1788
+ if (!this._button) {
1789
+ this._button = this._createUploadButton(this._find(this._element, 'button'));
1790
+ }
1791
+
1792
+ this._bindCancelAndRetryEvents();
1793
+
1794
+ this._dnd = this._setupDragAndDrop();
1795
+ };
1796
+
1797
+ // inherit from Basic Uploader
1798
+ qq.extend(qq.FineUploader.prototype, qq.FineUploaderBasic.prototype);
1799
+
1800
+ qq.extend(qq.FineUploader.prototype, {
1801
+ clearStoredFiles: function() {
1802
+ qq.FineUploaderBasic.prototype.clearStoredFiles.apply(this, arguments);
1803
+ this._listElement.innerHTML = "";
1804
+ },
1805
+ addExtraDropzone: function(element){
1806
+ this._dnd.setupExtraDropzone(element);
1807
+ },
1808
+ removeExtraDropzone: function(element){
1809
+ return this._dnd.removeExtraDropzone(element);
1810
+ },
1811
+ getItemByFileId: function(id){
1812
+ var item = this._listElement.firstChild;
1813
+
1814
+ // there can't be txt nodes in dynamically created list
1815
+ // and we can use nextSibling
1816
+ while (item){
1817
+ if (item.qqFileId == id) return item;
1818
+ item = item.nextSibling;
1819
+ }
1820
+ },
1821
+ cancel: function(fileId) {
1822
+ qq.FineUploaderBasic.prototype.cancel.apply(this, arguments);
1823
+ var item = this.getItemByFileId(fileId);
1824
+ qq(item).remove();
1825
+ },
1826
+ reset: function() {
1827
+ qq.FineUploaderBasic.prototype.reset.apply(this, arguments);
1828
+ this._element.innerHTML = this._options.template;
1829
+ this._listElement = this._options.listElement || this._find(this._element, 'list');
1830
+ if (!this._options.button) {
1831
+ this._button = this._createUploadButton(this._find(this._element, 'button'));
1832
+ }
1833
+ this._bindCancelAndRetryEvents();
1834
+ this._dnd.dispose();
1835
+ this._dnd = this._setupDragAndDrop();
1836
+ },
1837
+ _setupDragAndDrop: function() {
1838
+ var self = this,
1839
+ dropProcessingEl = this._find(this._element, 'dropProcessing'),
1840
+ dnd, preventSelectFiles, defaultDropAreaEl;
1841
+
1842
+ preventSelectFiles = function(event) {
1843
+ event.preventDefault();
1844
+ };
1845
+
1846
+ if (!this._options.dragAndDrop.disableDefaultDropzone) {
1847
+ defaultDropAreaEl = this._find(this._options.element, 'drop');
1848
+ }
1849
+
1850
+ dnd = new qq.DragAndDrop({
1851
+ dropArea: defaultDropAreaEl,
1852
+ extraDropzones: this._options.dragAndDrop.extraDropzones,
1853
+ hideDropzones: this._options.dragAndDrop.hideDropzones,
1854
+ multiple: this._options.multiple,
1855
+ classes: {
1856
+ dropActive: this._options.classes.dropActive
1857
+ },
1858
+ callbacks: {
1859
+ dropProcessing: function(isProcessing, files) {
1860
+ var input = self._button.getInput();
1861
+
1862
+ if (isProcessing) {
1863
+ qq(dropProcessingEl).css({display: 'block'});
1864
+ qq(input).attach('click', preventSelectFiles);
1865
+ }
1866
+ else {
1867
+ qq(dropProcessingEl).hide();
1868
+ qq(input).detach('click', preventSelectFiles);
1869
+ }
1870
+
1871
+ if (files) {
1872
+ self.addFiles(files);
1873
+ }
1874
+ },
1875
+ error: function(code, filename) {
1876
+ self._error(code, filename);
1877
+ },
1878
+ log: function(message, level) {
1879
+ self.log(message, level);
1880
+ }
1881
+ }
1882
+ });
1883
+
1884
+ dnd.setup();
1885
+
1886
+ return dnd;
1887
+ },
1888
+ _leaving_document_out: function(e){
1889
+ return ((qq.chrome() || (qq.safari() && qq.windows())) && e.clientX == 0 && e.clientY == 0) // null coords for Chrome and Safari Windows
1890
+ || (qq.firefox() && !e.relatedTarget); // null e.relatedTarget for Firefox
1891
+ },
1892
+ _storeFileForLater: function(id) {
1893
+ qq.FineUploaderBasic.prototype._storeFileForLater.apply(this, arguments);
1894
+ var item = this.getItemByFileId(id);
1895
+ qq(this._find(item, 'spinner')).hide();
1896
+ },
1897
+ /**
1898
+ * Gets one of the elements listed in this._options.classes
1899
+ **/
1900
+ _find: function(parent, type){
1901
+ var element = qq(parent).getByClass(this._options.classes[type])[0];
1902
+ if (!element){
1903
+ throw new Error('element not found ' + type);
1904
+ }
1905
+
1906
+ return element;
1907
+ },
1908
+ _onSubmit: function(id, fileName){
1909
+ qq.FineUploaderBasic.prototype._onSubmit.apply(this, arguments);
1910
+ this._addToList(id, fileName);
1911
+ },
1912
+ // Update the progress bar & percentage as the file is uploaded
1913
+ _onProgress: function(id, fileName, loaded, total){
1914
+ qq.FineUploaderBasic.prototype._onProgress.apply(this, arguments);
1915
+
1916
+ var item, progressBar, text, percent, cancelLink, size;
1917
+
1918
+ item = this.getItemByFileId(id);
1919
+ progressBar = this._find(item, 'progressBar');
1920
+ percent = Math.round(loaded / total * 100);
1921
+
1922
+ if (loaded === total) {
1923
+ cancelLink = this._find(item, 'cancel');
1924
+ qq(cancelLink).hide();
1925
+
1926
+ qq(progressBar).hide();
1927
+ qq(this._find(item, 'statusText')).setText(this._options.text.waitingForResponse);
1928
+
1929
+ // If last byte was sent, just display final size
1930
+ text = this._formatSize(total);
1931
+ }
1932
+ else {
1933
+ // If still uploading, display percentage
1934
+ text = this._formatProgress(loaded, total);
1935
+
1936
+ qq(progressBar).css({display: 'block'});
1937
+ }
1938
+
1939
+ // Update progress bar element
1940
+ qq(progressBar).css({width: percent + '%'});
1941
+
1942
+ size = this._find(item, 'size');
1943
+ qq(size).css({display: 'inline'});
1944
+ qq(size).setText(text);
1945
+ },
1946
+ _onComplete: function(id, fileName, result, xhr){
1947
+ qq.FineUploaderBasic.prototype._onComplete.apply(this, arguments);
1948
+
1949
+ var item = this.getItemByFileId(id);
1950
+
1951
+ qq(this._find(item, 'statusText')).clearText();
1952
+
1953
+ qq(item).removeClass(this._classes.retrying);
1954
+ qq(this._find(item, 'progressBar')).hide();
1955
+
1956
+ if (!this._options.disableCancelForFormUploads || qq.isXhrUploadSupported()) {
1957
+ qq(this._find(item, 'cancel')).hide();
1958
+ }
1959
+ qq(this._find(item, 'spinner')).hide();
1960
+
1961
+ if (result.success){
1962
+ qq(item).addClass(this._classes.success);
1963
+ if (this._classes.successIcon) {
1964
+ this._find(item, 'finished').style.display = "inline-block";
1965
+ qq(item).addClass(this._classes.successIcon);
1966
+ }
1967
+ } else {
1968
+ qq(item).addClass(this._classes.fail);
1969
+ if (this._classes.failIcon) {
1970
+ this._find(item, 'finished').style.display = "inline-block";
1971
+ qq(item).addClass(this._classes.failIcon);
1972
+ }
1973
+ if (this._options.retry.showButton && !this._preventRetries[id]) {
1974
+ qq(item).addClass(this._classes.retryable);
1975
+ }
1976
+ this._controlFailureTextDisplay(item, result);
1977
+ }
1978
+ },
1979
+ _onUpload: function(id, fileName){
1980
+ qq.FineUploaderBasic.prototype._onUpload.apply(this, arguments);
1981
+
1982
+ var item = this.getItemByFileId(id);
1983
+ this._showSpinner(item);
1984
+ },
1985
+ _onBeforeAutoRetry: function(id) {
1986
+ var item, progressBar, cancelLink, failTextEl, retryNumForDisplay, maxAuto, retryNote;
1987
+
1988
+ qq.FineUploaderBasic.prototype._onBeforeAutoRetry.apply(this, arguments);
1989
+
1990
+ item = this.getItemByFileId(id);
1991
+ progressBar = this._find(item, 'progressBar');
1992
+
1993
+ this._showCancelLink(item);
1994
+ progressBar.style.width = 0;
1995
+ qq(progressBar).hide();
1996
+
1997
+ if (this._options.retry.showAutoRetryNote) {
1998
+ failTextEl = this._find(item, 'statusText');
1999
+ retryNumForDisplay = this._autoRetries[id] + 1;
2000
+ maxAuto = this._options.retry.maxAutoAttempts;
2001
+
2002
+ retryNote = this._options.retry.autoRetryNote.replace(/\{retryNum\}/g, retryNumForDisplay);
2003
+ retryNote = retryNote.replace(/\{maxAuto\}/g, maxAuto);
2004
+
2005
+ qq(failTextEl).setText(retryNote);
2006
+ if (retryNumForDisplay === 1) {
2007
+ qq(item).addClass(this._classes.retrying);
2008
+ }
2009
+ }
2010
+ },
2011
+ //return false if we should not attempt the requested retry
2012
+ _onBeforeManualRetry: function(id) {
2013
+ if (qq.FineUploaderBasic.prototype._onBeforeManualRetry.apply(this, arguments)) {
2014
+ var item = this.getItemByFileId(id);
2015
+ this._find(item, 'progressBar').style.width = 0;
2016
+ qq(item).removeClass(this._classes.fail);
2017
+ qq(this._find(item, 'statusText')).clearText();
2018
+ this._showSpinner(item);
2019
+ this._showCancelLink(item);
2020
+ return true;
2021
+ }
2022
+ return false;
2023
+ },
2024
+ _addToList: function(id, fileName){
2025
+ var item = qq.toElement(this._options.fileTemplate);
2026
+ if (this._options.disableCancelForFormUploads && !qq.isXhrUploadSupported()) {
2027
+ var cancelLink = this._find(item, 'cancel');
2028
+ qq(cancelLink).remove();
2029
+ }
2030
+
2031
+ item.qqFileId = id;
2032
+
2033
+ var fileElement = this._find(item, 'file');
2034
+ qq(fileElement).setText(this._options.formatFileName(fileName));
2035
+ qq(this._find(item, 'size')).hide();
2036
+ if (!this._options.multiple) this._clearList();
2037
+ this._listElement.appendChild(item);
2038
+ },
2039
+ _clearList: function(){
2040
+ this._listElement.innerHTML = '';
2041
+ this.clearStoredFiles();
2042
+ },
2043
+ /**
2044
+ * delegate click event for cancel & retry links
2045
+ **/
2046
+ _bindCancelAndRetryEvents: function(){
2047
+ var self = this,
2048
+ list = this._listElement;
2049
+
2050
+ this._disposeSupport.attach(list, 'click', function(e){
2051
+ e = e || window.event;
2052
+ var target = e.target || e.srcElement;
2053
+
2054
+ if (qq(target).hasClass(self._classes.cancel) || qq(target).hasClass(self._classes.retry)){
2055
+ qq.preventDefault(e);
2056
+
2057
+ var item = target.parentNode;
2058
+ while(item.qqFileId == undefined) {
2059
+ item = target = target.parentNode;
2060
+ }
2061
+
2062
+ if (qq(target).hasClass(self._classes.cancel)) {
2063
+ self.cancel(item.qqFileId);
2064
+ }
2065
+ else {
2066
+ qq(item).removeClass(self._classes.retryable);
2067
+ self.retry(item.qqFileId);
2068
+ }
2069
+ }
2070
+ });
2071
+ },
2072
+ _formatProgress: function (uploadedSize, totalSize) {
2073
+ var message = this._options.text.formatProgress;
2074
+ function r(name, replacement) { message = message.replace(name, replacement); }
2075
+
2076
+ r('{percent}', Math.round(uploadedSize / totalSize * 100));
2077
+ r('{total_size}', this._formatSize(totalSize));
2078
+ return message;
2079
+ },
2080
+ _controlFailureTextDisplay: function(item, response) {
2081
+ var mode, maxChars, responseProperty, failureReason, shortFailureReason;
2082
+
2083
+ mode = this._options.failedUploadTextDisplay.mode;
2084
+ maxChars = this._options.failedUploadTextDisplay.maxChars;
2085
+ responseProperty = this._options.failedUploadTextDisplay.responseProperty;
2086
+
2087
+ if (mode === 'custom') {
2088
+ failureReason = response[responseProperty];
2089
+ if (failureReason) {
2090
+ if (failureReason.length > maxChars) {
2091
+ shortFailureReason = failureReason.substring(0, maxChars) + '...';
2092
+ }
2093
+ }
2094
+ else {
2095
+ failureReason = this._options.text.failUpload;
2096
+ this.log("'" + responseProperty + "' is not a valid property on the server response.", 'warn');
2097
+ }
2098
+
2099
+ qq(this._find(item, 'statusText')).setText(shortFailureReason || failureReason);
2100
+
2101
+ if (this._options.failedUploadTextDisplay.enableTooltip) {
2102
+ this._showTooltip(item, failureReason);
2103
+ }
2104
+ }
2105
+ else if (mode === 'default') {
2106
+ qq(this._find(item, 'statusText')).setText(this._options.text.failUpload);
2107
+ }
2108
+ else if (mode !== 'none') {
2109
+ this.log("failedUploadTextDisplay.mode value of '" + mode + "' is not valid", 'warn');
2110
+ }
2111
+ },
2112
+ //TODO turn this into a real tooltip, with click trigger (so it is usable on mobile devices). See case #355 for details.
2113
+ _showTooltip: function(item, text) {
2114
+ item.title = text;
2115
+ },
2116
+ _showSpinner: function(item) {
2117
+ var spinnerEl = this._find(item, 'spinner');
2118
+ spinnerEl.style.display = "inline-block";
2119
+ },
2120
+ _showCancelLink: function(item) {
2121
+ if (!this._options.disableCancelForFormUploads || qq.isXhrUploadSupported()) {
2122
+ var cancelLink = this._find(item, 'cancel');
2123
+ cancelLink.style.display = 'inline';
2124
+ }
2125
+ },
2126
+ _error: function(code, fileName){
2127
+ var message = qq.FineUploaderBasic.prototype._error.apply(this, arguments);
2128
+ this._options.showMessage(message);
2129
+ }
2130
+ });
2131
+ /**
2132
+ * Class for uploading files, uploading itself is handled by child classes
2133
+ */
2134
+ /*globals qq*/
2135
+ qq.UploadHandler = function(o) {
2136
+ "use strict";
2137
+
2138
+ var queue = [],
2139
+ options, log, dequeue, handlerImpl;
2140
+
2141
+ // Default options, can be overridden by the user
2142
+ options = {
2143
+ debug: false,
2144
+ forceMultipart: true,
2145
+ paramsInBody: false,
2146
+ paramsStore: {},
2147
+ endpointStore: {},
2148
+ maxConnections: 3, // maximum number of concurrent uploads
2149
+ uuidParamName: 'qquuid',
2150
+ totalFileSizeParamName: 'qqtotalfilesize',
2151
+ chunking: {
2152
+ enabled: false,
2153
+ partSize: 2000000, //bytes
2154
+ paramNames: {
2155
+ partIndex: 'qqpartindex',
2156
+ partByteOffset: 'qqpartbyteoffset',
2157
+ chunkSize: 'qqchunksize',
2158
+ totalParts: 'qqtotalparts',
2159
+ filename: 'qqfilename'
2160
+ }
2161
+ },
2162
+ resume: {
2163
+ enabled: false,
2164
+ id: null,
2165
+ cookiesExpireIn: 7, //days
2166
+ paramNames: {
2167
+ resuming: "qqresume"
2168
+ }
2169
+ },
2170
+ log: function(str, level) {},
2171
+ onProgress: function(id, fileName, loaded, total){},
2172
+ onComplete: function(id, fileName, response, xhr){},
2173
+ onCancel: function(id, fileName){},
2174
+ onUpload: function(id, fileName){},
2175
+ onUploadChunk: function(id, fileName, chunkData){},
2176
+ onAutoRetry: function(id, fileName, response, xhr){},
2177
+ onResume: function(id, fileName, chunkData){}
2178
+
2179
+ };
2180
+ qq.extend(options, o);
2181
+
2182
+ log = options.log;
2183
+
2184
+ /**
2185
+ * Removes element from queue, starts upload of next
2186
+ */
2187
+ dequeue = function(id) {
2188
+ var i = qq.indexOf(queue, id),
2189
+ max = options.maxConnections,
2190
+ nextId;
2191
+
2192
+ queue.splice(i, 1);
2193
+
2194
+ if (queue.length >= max && i < max){
2195
+ nextId = queue[max-1];
2196
+ handlerImpl.upload(nextId);
2197
+ }
2198
+ };
2199
+
2200
+ if (qq.isXhrUploadSupported()) {
2201
+ handlerImpl = new qq.UploadHandlerXhr(options, dequeue, log);
2202
+ }
2203
+ else {
2204
+ handlerImpl = new qq.UploadHandlerForm(options, dequeue, log);
2205
+ }
2206
+
2207
+
2208
+ return {
2209
+ /**
2210
+ * Adds file or file input to the queue
2211
+ * @returns id
2212
+ **/
2213
+ add: function(file){
2214
+ return handlerImpl.add(file);
2215
+ },
2216
+ /**
2217
+ * Sends the file identified by id
2218
+ */
2219
+ upload: function(id){
2220
+ var len = queue.push(id);
2221
+
2222
+ // if too many active uploads, wait...
2223
+ if (len <= options.maxConnections){
2224
+ return handlerImpl.upload(id);
2225
+ }
2226
+ },
2227
+ retry: function(id) {
2228
+ var i = qq.indexOf(queue, id);
2229
+ if (i >= 0) {
2230
+ return handlerImpl.upload(id, true);
2231
+ }
2232
+ else {
2233
+ return this.upload(id);
2234
+ }
2235
+ },
2236
+ /**
2237
+ * Cancels file upload by id
2238
+ */
2239
+ cancel: function(id){
2240
+ log('Cancelling ' + id);
2241
+ options.paramsStore.remove(id);
2242
+ handlerImpl.cancel(id);
2243
+ dequeue(id);
2244
+ },
2245
+ /**
2246
+ * Cancels all uploads
2247
+ */
2248
+ cancelAll: function(){
2249
+ qq.each(queue, function(idx, fileId) {
2250
+ this.cancel(fileId);
2251
+ });
2252
+
2253
+ queue = [];
2254
+ },
2255
+ /**
2256
+ * Returns name of the file identified by id
2257
+ */
2258
+ getName: function(id){
2259
+ return handlerImpl.getName(id);
2260
+ },
2261
+ /**
2262
+ * Returns size of the file identified by id
2263
+ */
2264
+ getSize: function(id){
2265
+ if (handlerImpl.getSize) {
2266
+ return handlerImpl.getSize(id);
2267
+ }
2268
+ },
2269
+ getFile: function(id) {
2270
+ if (handlerImpl.getFile) {
2271
+ return handlerImpl.getFile(id);
2272
+ }
2273
+ },
2274
+ /**
2275
+ * Returns id of files being uploaded or
2276
+ * waiting for their turn
2277
+ */
2278
+ getQueue: function(){
2279
+ return queue;
2280
+ },
2281
+ reset: function() {
2282
+ log('Resetting upload handler');
2283
+ queue = [];
2284
+ handlerImpl.reset();
2285
+ },
2286
+ getUuid: function(id) {
2287
+ return handlerImpl.getUuid(id);
2288
+ },
2289
+ /**
2290
+ * Determine if the file exists.
2291
+ */
2292
+ isValid: function(id) {
2293
+ return handlerImpl.isValid(id);
2294
+ },
2295
+ getResumableFilesData: function() {
2296
+ if (handlerImpl.getResumableFilesData) {
2297
+ return handlerImpl.getResumableFilesData();
2298
+ }
2299
+ return [];
2300
+ }
2301
+ };
2302
+ };
2303
+ /*globals qq, document, setTimeout*/
2304
+ /*jslint white: true*/
2305
+ qq.UploadHandlerForm = function(o, uploadCompleteCallback, logCallback) {
2306
+ "use strict";
2307
+
2308
+ var options = o,
2309
+ inputs = [],
2310
+ uuids = [],
2311
+ detachLoadEvents = {},
2312
+ uploadComplete = uploadCompleteCallback,
2313
+ log = logCallback,
2314
+ api;
2315
+
2316
+ function attachLoadEvent(iframe, callback) {
2317
+ /*jslint eqeq: true*/
2318
+
2319
+ detachLoadEvents[iframe.id] = qq(iframe).attach('load', function(){
2320
+ log('Received response for ' + iframe.id);
2321
+
2322
+ // when we remove iframe from dom
2323
+ // the request stops, but in IE load
2324
+ // event fires
2325
+ if (!iframe.parentNode){
2326
+ return;
2327
+ }
2328
+
2329
+ try {
2330
+ // fixing Opera 10.53
2331
+ if (iframe.contentDocument &&
2332
+ iframe.contentDocument.body &&
2333
+ iframe.contentDocument.body.innerHTML == "false"){
2334
+ // In Opera event is fired second time
2335
+ // when body.innerHTML changed from false
2336
+ // to server response approx. after 1 sec
2337
+ // when we upload file with iframe
2338
+ return;
2339
+ }
2340
+ }
2341
+ catch (error) {
2342
+ //IE may throw an "access is denied" error when attempting to access contentDocument on the iframe in some cases
2343
+ log('Error when attempting to access iframe during handling of upload response (' + error + ")", 'error');
2344
+ }
2345
+
2346
+ callback();
2347
+ });
2348
+ }
2349
+
2350
+ /**
2351
+ * Returns json object received by iframe from server.
2352
+ */
2353
+ function getIframeContentJson(iframe) {
2354
+ /*jshint evil: true*/
2355
+
2356
+ var response;
2357
+
2358
+ //IE may throw an "access is denied" error when attempting to access contentDocument on the iframe in some cases
2359
+ try {
2360
+ // iframe.contentWindow.document - for IE<7
2361
+ var doc = iframe.contentDocument || iframe.contentWindow.document,
2362
+ innerHTML = doc.body.innerHTML;
2363
+
2364
+ log("converting iframe's innerHTML to JSON");
2365
+ log("innerHTML = " + innerHTML);
2366
+ //plain text response may be wrapped in <pre> tag
2367
+ if (innerHTML && innerHTML.match(/^<pre/i)) {
2368
+ innerHTML = doc.body.firstChild.firstChild.nodeValue;
2369
+ }
2370
+ response = eval("(" + innerHTML + ")");
2371
+ } catch(error){
2372
+ log('Error when attempting to parse form upload response (' + error + ")", 'error');
2373
+ response = {success: false};
2374
+ }
2375
+
2376
+ return response;
2377
+ }
2378
+
2379
+ /**
2380
+ * Creates iframe with unique name
2381
+ */
2382
+ function createIframe(id){
2383
+ // We can't use following code as the name attribute
2384
+ // won't be properly registered in IE6, and new window
2385
+ // on form submit will open
2386
+ // var iframe = document.createElement('iframe');
2387
+ // iframe.setAttribute('name', id);
2388
+
2389
+ var iframe = qq.toElement('<iframe src="javascript:false;" name="' + id + '" />');
2390
+ // src="javascript:false;" removes ie6 prompt on https
2391
+
2392
+ iframe.setAttribute('id', id);
2393
+
2394
+ iframe.style.display = 'none';
2395
+ document.body.appendChild(iframe);
2396
+
2397
+ return iframe;
2398
+ }
2399
+
2400
+ /**
2401
+ * Creates form, that will be submitted to iframe
2402
+ */
2403
+ function createForm(id, iframe){
2404
+ var params = options.paramsStore.getParams(id),
2405
+ protocol = options.demoMode ? "GET" : "POST",
2406
+ form = qq.toElement('<form method="' + protocol + '" enctype="multipart/form-data"></form>'),
2407
+ endpoint = options.endpointStore.getEndpoint(id),
2408
+ url = endpoint;
2409
+
2410
+ params[options.uuidParamName] = uuids[id];
2411
+
2412
+ if (!options.paramsInBody) {
2413
+ url = qq.obj2url(params, endpoint);
2414
+ }
2415
+ else {
2416
+ qq.obj2Inputs(params, form);
2417
+ }
2418
+
2419
+ form.setAttribute('action', url);
2420
+ form.setAttribute('target', iframe.name);
2421
+ form.style.display = 'none';
2422
+ document.body.appendChild(form);
2423
+
2424
+ return form;
2425
+ }
2426
+
2427
+
2428
+ api = {
2429
+ add: function(fileInput) {
2430
+ fileInput.setAttribute('name', options.inputName);
2431
+
2432
+ var id = inputs.push(fileInput) - 1;
2433
+ uuids[id] = qq.getUniqueId();
2434
+
2435
+ // remove file input from DOM
2436
+ if (fileInput.parentNode){
2437
+ qq(fileInput).remove();
2438
+ }
2439
+
2440
+ return id;
2441
+ },
2442
+ getName: function(id) {
2443
+ /*jslint regexp: true*/
2444
+
2445
+ // get input value and remove path to normalize
2446
+ return inputs[id].value.replace(/.*(\/|\\)/, "");
2447
+ },
2448
+ isValid: function(id) {
2449
+ return inputs[id] !== undefined;
2450
+ },
2451
+ reset: function() {
2452
+ qq.UploadHandler.prototype.reset.apply(this, arguments);
2453
+ inputs = [];
2454
+ uuids = [];
2455
+ detachLoadEvents = {};
2456
+ },
2457
+ getUuid: function(id) {
2458
+ return uuids[id];
2459
+ },
2460
+ cancel: function(id) {
2461
+ options.onCancel(id, this.getName(id));
2462
+
2463
+ delete inputs[id];
2464
+ delete uuids[id];
2465
+ delete detachLoadEvents[id];
2466
+
2467
+ var iframe = document.getElementById(id);
2468
+ if (iframe) {
2469
+ // to cancel request set src to something else
2470
+ // we use src="javascript:false;" because it doesn't
2471
+ // trigger ie6 prompt on https
2472
+ iframe.setAttribute('src', 'java' + String.fromCharCode(115) + 'cript:false;'); //deal with "JSLint: javascript URL" warning, which apparently cannot be turned off
2473
+
2474
+ qq(iframe).remove();
2475
+ }
2476
+ },
2477
+ upload: function(id){
2478
+ var input = inputs[id],
2479
+ fileName = api.getName(id),
2480
+ iframe = createIframe(id),
2481
+ form = createForm(id, iframe);
2482
+
2483
+ if (!input){
2484
+ throw new Error('file with passed id was not added, or already uploaded or cancelled');
2485
+ }
2486
+
2487
+ options.onUpload(id, this.getName(id));
2488
+
2489
+ form.appendChild(input);
2490
+
2491
+ attachLoadEvent(iframe, function(){
2492
+ log('iframe loaded');
2493
+
2494
+ var response = getIframeContentJson(iframe);
2495
+
2496
+ // timeout added to fix busy state in FF3.6
2497
+ setTimeout(function(){
2498
+ detachLoadEvents[id]();
2499
+ delete detachLoadEvents[id];
2500
+ qq(iframe).remove();
2501
+ }, 1);
2502
+
2503
+ if (!response.success) {
2504
+ if (options.onAutoRetry(id, fileName, response)) {
2505
+ return;
2506
+ }
2507
+ }
2508
+ options.onComplete(id, fileName, response);
2509
+ uploadComplete(id);
2510
+ });
2511
+
2512
+ log('Sending upload request for ' + id);
2513
+ form.submit();
2514
+ qq(form).remove();
2515
+
2516
+ return id;
2517
+ }
2518
+ };
2519
+
2520
+ return api;
2521
+ };
2522
+ /*globals qq, File, XMLHttpRequest, FormData*/
2523
+ qq.UploadHandlerXhr = function(o, uploadCompleteCallback, logCallback) {
2524
+ "use strict";
2525
+
2526
+ var options = o,
2527
+ uploadComplete = uploadCompleteCallback,
2528
+ log = logCallback,
2529
+ fileState = [],
2530
+ cookieItemDelimiter = "|",
2531
+ chunkFiles = options.chunking.enabled && qq.isFileChunkingSupported(),
2532
+ resumeEnabled = options.resume.enabled && chunkFiles && qq.areCookiesEnabled(),
2533
+ resumeId = getResumeId(),
2534
+ multipart = options.forceMultipart || options.paramsInBody,
2535
+ api;
2536
+
2537
+
2538
+ function addChunkingSpecificParams(id, params, chunkData) {
2539
+ var size = api.getSize(id),
2540
+ name = api.getName(id);
2541
+
2542
+ params[options.chunking.paramNames.partIndex] = chunkData.part;
2543
+ params[options.chunking.paramNames.partByteOffset] = chunkData.start;
2544
+ params[options.chunking.paramNames.chunkSize] = chunkData.end - chunkData.start;
2545
+ params[options.chunking.paramNames.totalParts] = chunkData.count;
2546
+ params[options.totalFileSizeParamName] = size;
2547
+
2548
+
2549
+ /**
2550
+ * When a Blob is sent in a multipart request, the filename value in the content-disposition header is either "blob"
2551
+ * or an empty string. So, we will need to include the actual file name as a param in this case.
2552
+ */
2553
+ if (multipart) {
2554
+ params[options.chunking.paramNames.filename] = name;
2555
+ }
2556
+ }
2557
+
2558
+ function addResumeSpecificParams(params) {
2559
+ params[options.resume.paramNames.resuming] = true;
2560
+ }
2561
+
2562
+ function getChunk(file, startByte, endByte) {
2563
+ if (file.slice) {
2564
+ return file.slice(startByte, endByte);
2565
+ }
2566
+ else if (file.mozSlice) {
2567
+ return file.mozSlice(startByte, endByte);
2568
+ }
2569
+ else if (file.webkitSlice) {
2570
+ return file.webkitSlice(startByte, endByte);
2571
+ }
2572
+ }
2573
+
2574
+ function getChunkData(id, chunkIndex) {
2575
+ var chunkSize = options.chunking.partSize,
2576
+ fileSize = api.getSize(id),
2577
+ file = fileState[id].file,
2578
+ startBytes = chunkSize * chunkIndex,
2579
+ endBytes = startBytes+chunkSize >= fileSize ? fileSize : startBytes+chunkSize,
2580
+ totalChunks = getTotalChunks(id);
2581
+
2582
+ return {
2583
+ part: chunkIndex,
2584
+ start: startBytes,
2585
+ end: endBytes,
2586
+ count: totalChunks,
2587
+ blob: getChunk(file, startBytes, endBytes)
2588
+ };
2589
+ }
2590
+
2591
+ function getTotalChunks(id) {
2592
+ var fileSize = api.getSize(id),
2593
+ chunkSize = options.chunking.partSize;
2594
+
2595
+ return Math.ceil(fileSize / chunkSize);
2596
+ }
2597
+
2598
+ function createXhr(id) {
2599
+ fileState[id].xhr = new XMLHttpRequest();
2600
+ return fileState[id].xhr;
2601
+ }
2602
+
2603
+ function setParamsAndGetEntityToSend(params, xhr, fileOrBlob, id) {
2604
+ var formData = new FormData(),
2605
+ protocol = options.demoMode ? "GET" : "POST",
2606
+ endpoint = options.endpointStore.getEndpoint(id),
2607
+ url = endpoint,
2608
+ name = api.getName(id),
2609
+ size = api.getSize(id);
2610
+
2611
+ params[options.uuidParamName] = fileState[id].uuid;
2612
+
2613
+ if (multipart) {
2614
+ params[options.totalFileSizeParamName] = size;
2615
+ }
2616
+
2617
+ //build query string
2618
+ if (!options.paramsInBody) {
2619
+ params[options.inputName] = name;
2620
+ url = qq.obj2url(params, endpoint);
2621
+ }
2622
+
2623
+ xhr.open(protocol, url, true);
2624
+ if (multipart) {
2625
+ if (options.paramsInBody) {
2626
+ qq.obj2FormData(params, formData);
2627
+ }
2628
+
2629
+ formData.append(options.inputName, fileOrBlob);
2630
+ return formData;
2631
+ }
2632
+
2633
+ return fileOrBlob;
2634
+ }
2635
+
2636
+ function setHeaders(id, xhr) {
2637
+ var extraHeaders = options.customHeaders,
2638
+ name = api.getName(id),
2639
+ file = fileState[id].file;
2640
+
2641
+ xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
2642
+ xhr.setRequestHeader("Cache-Control", "no-cache");
2643
+
2644
+ if (!multipart) {
2645
+ xhr.setRequestHeader("Content-Type", "application/octet-stream");
2646
+ //NOTE: return mime type in xhr works on chrome 16.0.9 firefox 11.0a2
2647
+ xhr.setRequestHeader("X-Mime-Type", file.type);
2648
+ }
2649
+
2650
+ qq.each(extraHeaders, function(name, val) {
2651
+ xhr.setRequestHeader(name, val);
2652
+ });
2653
+ }
2654
+
2655
+ function handleCompletedFile(id, response, xhr) {
2656
+ var name = api.getName(id),
2657
+ size = api.getSize(id);
2658
+
2659
+ fileState[id].attemptingResume = false;
2660
+
2661
+ options.onProgress(id, name, size, size);
2662
+
2663
+ options.onComplete(id, name, response, xhr);
2664
+ delete fileState[id].xhr;
2665
+ uploadComplete(id);
2666
+ }
2667
+
2668
+ function uploadNextChunk(id) {
2669
+ var chunkData = getChunkData(id, fileState[id].remainingChunkIdxs[0]),
2670
+ xhr = createXhr(id),
2671
+ size = api.getSize(id),
2672
+ name = api.getName(id),
2673
+ toSend, params;
2674
+
2675
+ if (fileState[id].loaded === undefined) {
2676
+ fileState[id].loaded = 0;
2677
+ }
2678
+
2679
+ persistChunkData(id, chunkData);
2680
+
2681
+ xhr.onreadystatechange = getReadyStateChangeHandler(id, xhr);
2682
+
2683
+ xhr.upload.onprogress = function(e) {
2684
+ if (e.lengthComputable) {
2685
+ if (fileState[id].loaded < size) {
2686
+ var totalLoaded = e.loaded + fileState[id].loaded;
2687
+ options.onProgress(id, name, totalLoaded, size);
2688
+ }
2689
+ }
2690
+ };
2691
+
2692
+ options.onUploadChunk(id, name, getChunkDataForCallback(chunkData));
2693
+
2694
+ params = options.paramsStore.getParams(id);
2695
+ addChunkingSpecificParams(id, params, chunkData);
2696
+
2697
+ if (fileState[id].attemptingResume) {
2698
+ addResumeSpecificParams(params);
2699
+ }
2700
+
2701
+ toSend = setParamsAndGetEntityToSend(params, xhr, chunkData.blob, id);
2702
+ setHeaders(id, xhr);
2703
+
2704
+ log('Sending chunked upload request for ' + id + ": bytes " + (chunkData.start+1) + "-" + chunkData.end + " of " + size);
2705
+ xhr.send(toSend);
2706
+ }
2707
+
2708
+
2709
+ function handleSuccessfullyCompletedChunk(id, response, xhr) {
2710
+ var chunkIdx = fileState[id].remainingChunkIdxs.shift(),
2711
+ chunkData = getChunkData(id, chunkIdx);
2712
+
2713
+ fileState[id].attemptingResume = false;
2714
+ fileState[id].loaded += chunkData.end - chunkData.start;
2715
+
2716
+ if (fileState[id].remainingChunkIdxs.length > 0) {
2717
+ uploadNextChunk(id);
2718
+ }
2719
+ else {
2720
+ deletePersistedChunkData(id);
2721
+ handleCompletedFile(id, response, xhr);
2722
+ }
2723
+ }
2724
+
2725
+ function isErrorResponse(xhr, response) {
2726
+ return xhr.status !== 200 || !response.success || response.reset;
2727
+ }
2728
+
2729
+ function parseResponse(xhr) {
2730
+ var response;
2731
+
2732
+ try {
2733
+ response = qq.parseJson(xhr.responseText);
2734
+ }
2735
+ catch(error) {
2736
+ log('Error when attempting to parse xhr response text (' + error + ')', 'error');
2737
+ response = {};
2738
+ }
2739
+
2740
+ return response;
2741
+ }
2742
+
2743
+ function handleResetResponse(id) {
2744
+ log('Server has ordered chunking effort to be restarted on next attempt for file ID ' + id, 'error');
2745
+
2746
+ if (resumeEnabled) {
2747
+ deletePersistedChunkData(id);
2748
+ }
2749
+ fileState[id].remainingChunkIdxs = [];
2750
+ delete fileState[id].loaded;
2751
+ }
2752
+
2753
+ function handleResetResponseOnResumeAttempt(id) {
2754
+ fileState[id].attemptingResume = false;
2755
+ log("Server has declared that it cannot handle resume for file ID " + id + " - starting from the first chunk", 'error');
2756
+ api.upload(id, true);
2757
+ }
2758
+
2759
+ function handleNonResetErrorResponse(id, response, xhr) {
2760
+ var name = api.getName(id);
2761
+
2762
+ if (options.onAutoRetry(id, name, response, xhr)) {
2763
+ return;
2764
+ }
2765
+ else {
2766
+ handleCompletedFile(id, response, xhr);
2767
+ }
2768
+ }
2769
+
2770
+ function onComplete(id, xhr) {
2771
+ var response;
2772
+
2773
+ // the request was aborted/cancelled
2774
+ if (!fileState[id]) {
2775
+ return;
2776
+ }
2777
+
2778
+ log("xhr - server response received for " + id);
2779
+ log("responseText = " + xhr.responseText);
2780
+ response = parseResponse(xhr);
2781
+
2782
+ if (isErrorResponse(xhr, response)) {
2783
+ if (response.reset) {
2784
+ handleResetResponse(id);
2785
+ }
2786
+
2787
+ if (fileState[id].attemptingResume && response.reset) {
2788
+ handleResetResponseOnResumeAttempt(id);
2789
+ }
2790
+ else {
2791
+ handleNonResetErrorResponse(id, response, xhr);
2792
+ }
2793
+ }
2794
+ else if (chunkFiles) {
2795
+ handleSuccessfullyCompletedChunk(id, response, xhr);
2796
+ }
2797
+ else {
2798
+ handleCompletedFile(id, response, xhr);
2799
+ }
2800
+ }
2801
+
2802
+ function getChunkDataForCallback(chunkData) {
2803
+ return {
2804
+ partIndex: chunkData.part,
2805
+ startByte: chunkData.start + 1,
2806
+ endByte: chunkData.end,
2807
+ totalParts: chunkData.count
2808
+ };
2809
+ }
2810
+
2811
+ function getReadyStateChangeHandler(id, xhr) {
2812
+ return function() {
2813
+ if (xhr.readyState === 4) {
2814
+ onComplete(id, xhr);
2815
+ }
2816
+ };
2817
+ }
2818
+
2819
+ function persistChunkData(id, chunkData) {
2820
+ var fileUuid = api.getUuid(id),
2821
+ cookieName = getChunkDataCookieName(id),
2822
+ cookieValue = fileUuid + cookieItemDelimiter + chunkData.part,
2823
+ cookieExpDays = options.resume.cookiesExpireIn;
2824
+
2825
+ qq.setCookie(cookieName, cookieValue, cookieExpDays);
2826
+ }
2827
+
2828
+ function deletePersistedChunkData(id) {
2829
+ var cookieName = getChunkDataCookieName(id);
2830
+
2831
+ qq.deleteCookie(cookieName);
2832
+ }
2833
+
2834
+ function getPersistedChunkData(id) {
2835
+ var chunkCookieValue = qq.getCookie(getChunkDataCookieName(id)),
2836
+ delimiterIndex, uuid, partIndex;
2837
+
2838
+ if (chunkCookieValue) {
2839
+ delimiterIndex = chunkCookieValue.indexOf(cookieItemDelimiter);
2840
+ uuid = chunkCookieValue.substr(0, delimiterIndex);
2841
+ partIndex = parseInt(chunkCookieValue.substr(delimiterIndex + 1, chunkCookieValue.length - delimiterIndex), 10);
2842
+
2843
+ return {
2844
+ uuid: uuid,
2845
+ part: partIndex
2846
+ };
2847
+ }
2848
+ }
2849
+
2850
+ function getChunkDataCookieName(id) {
2851
+ var filename = api.getName(id),
2852
+ fileSize = api.getSize(id),
2853
+ maxChunkSize = options.chunking.partSize,
2854
+ cookieName;
2855
+
2856
+ cookieName = "qqfilechunk" + cookieItemDelimiter + encodeURIComponent(filename) + cookieItemDelimiter + fileSize + cookieItemDelimiter + maxChunkSize;
2857
+
2858
+ if (resumeId !== undefined) {
2859
+ cookieName += cookieItemDelimiter + resumeId;
2860
+ }
2861
+
2862
+ return cookieName;
2863
+ }
2864
+
2865
+ function getResumeId() {
2866
+ if (options.resume.id !== null &&
2867
+ options.resume.id !== undefined &&
2868
+ !qq.isFunction(options.resume.id) &&
2869
+ !qq.isObject(options.resume.id)) {
2870
+
2871
+ return options.resume.id;
2872
+ }
2873
+ }
2874
+
2875
+ function handleFileChunkingUpload(id, retry) {
2876
+ var name = api.getName(id),
2877
+ firstChunkIndex = 0,
2878
+ persistedChunkInfoForResume, firstChunkDataForResume, currentChunkIndex;
2879
+
2880
+ if (!fileState[id].remainingChunkIdxs || fileState[id].remainingChunkIdxs.length === 0) {
2881
+ fileState[id].remainingChunkIdxs = [];
2882
+
2883
+ if (resumeEnabled && !retry) {
2884
+ persistedChunkInfoForResume = getPersistedChunkData(id);
2885
+ if (persistedChunkInfoForResume) {
2886
+ firstChunkDataForResume = getChunkData(id, persistedChunkInfoForResume.part);
2887
+ if (options.onResume(id, name, getChunkDataForCallback(firstChunkDataForResume)) !== false) {
2888
+ firstChunkIndex = persistedChunkInfoForResume.part;
2889
+ fileState[id].uuid = persistedChunkInfoForResume.uuid;
2890
+ fileState[id].loaded = firstChunkDataForResume.start;
2891
+ fileState[id].attemptingResume = true;
2892
+ log('Resuming ' + name + " at partition index " + firstChunkIndex);
2893
+ }
2894
+ }
2895
+ }
2896
+
2897
+ for (currentChunkIndex = getTotalChunks(id)-1; currentChunkIndex >= firstChunkIndex; currentChunkIndex-=1) {
2898
+ fileState[id].remainingChunkIdxs.unshift(currentChunkIndex);
2899
+ }
2900
+ }
2901
+
2902
+ uploadNextChunk(id);
2903
+ }
2904
+
2905
+ function handleStandardFileUpload(id) {
2906
+ var file = fileState[id].file,
2907
+ name = api.getName(id),
2908
+ xhr, params, toSend;
2909
+
2910
+ fileState[id].loaded = 0;
2911
+
2912
+ xhr = createXhr(id);
2913
+
2914
+ xhr.upload.onprogress = function(e){
2915
+ if (e.lengthComputable){
2916
+ fileState[id].loaded = e.loaded;
2917
+ options.onProgress(id, name, e.loaded, e.total);
2918
+ }
2919
+ };
2920
+
2921
+ xhr.onreadystatechange = getReadyStateChangeHandler(id, xhr);
2922
+
2923
+ params = options.paramsStore.getParams(id);
2924
+ toSend = setParamsAndGetEntityToSend(params, xhr, file, id);
2925
+ setHeaders(id, xhr);
2926
+
2927
+ log('Sending upload request for ' + id);
2928
+ xhr.send(toSend);
2929
+ }
2930
+
2931
+
2932
+ api = {
2933
+ /**
2934
+ * Adds file to the queue
2935
+ * Returns id to use with upload, cancel
2936
+ **/
2937
+ add: function(file){
2938
+ if (!(file instanceof File)){
2939
+ throw new Error('Passed obj in not a File (in qq.UploadHandlerXhr)');
2940
+ }
2941
+
2942
+
2943
+ var id = fileState.push({file: file}) - 1;
2944
+ fileState[id].uuid = qq.getUniqueId();
2945
+
2946
+ return id;
2947
+ },
2948
+ getName: function(id){
2949
+ var file = fileState[id].file;
2950
+ // fix missing name in Safari 4
2951
+ //NOTE: fixed missing name firefox 11.0a2 file.fileName is actually undefined
2952
+ return (file.fileName !== null && file.fileName !== undefined) ? file.fileName : file.name;
2953
+ },
2954
+ getSize: function(id){
2955
+ /*jshint eqnull: true*/
2956
+ var file = fileState[id].file;
2957
+ return file.fileSize != null ? file.fileSize : file.size;
2958
+ },
2959
+ getFile: function(id) {
2960
+ if (fileState[id]) {
2961
+ return fileState[id].file;
2962
+ }
2963
+ },
2964
+ /**
2965
+ * Returns uploaded bytes for file identified by id
2966
+ */
2967
+ getLoaded: function(id){
2968
+ return fileState[id].loaded || 0;
2969
+ },
2970
+ isValid: function(id) {
2971
+ return fileState[id] !== undefined;
2972
+ },
2973
+ reset: function() {
2974
+ fileState = [];
2975
+ },
2976
+ getUuid: function(id) {
2977
+ return fileState[id].uuid;
2978
+ },
2979
+ /**
2980
+ * Sends the file identified by id to the server
2981
+ */
2982
+ upload: function(id, retry){
2983
+ var name = this.getName(id);
2984
+
2985
+ options.onUpload(id, name);
2986
+
2987
+ if (chunkFiles) {
2988
+ handleFileChunkingUpload(id, retry);
2989
+ }
2990
+ else {
2991
+ handleStandardFileUpload(id);
2992
+ }
2993
+ },
2994
+ cancel: function(id){
2995
+ options.onCancel(id, this.getName(id));
2996
+
2997
+ if (fileState[id].xhr){
2998
+ fileState[id].xhr.abort();
2999
+ }
3000
+
3001
+ if (resumeEnabled) {
3002
+ deletePersistedChunkData(id);
3003
+ }
3004
+
3005
+ delete fileState[id];
3006
+ },
3007
+ getResumableFilesData: function() {
3008
+ var matchingCookieNames = [],
3009
+ resumableFilesData = [];
3010
+
3011
+ if (chunkFiles && resumeEnabled) {
3012
+ if (resumeId === undefined) {
3013
+ matchingCookieNames = qq.getCookieNames(new RegExp("^qqfilechunk\\" + cookieItemDelimiter + ".+\\" +
3014
+ cookieItemDelimiter + "\\d+\\" + cookieItemDelimiter + options.chunking.partSize + "="));
3015
+ }
3016
+ else {
3017
+ matchingCookieNames = qq.getCookieNames(new RegExp("^qqfilechunk\\" + cookieItemDelimiter + ".+\\" +
3018
+ cookieItemDelimiter + "\\d+\\" + cookieItemDelimiter + options.chunking.partSize + "\\" +
3019
+ cookieItemDelimiter + resumeId + "="));
3020
+ }
3021
+
3022
+ qq.each(matchingCookieNames, function(idx, cookieName) {
3023
+ var cookiesNameParts = cookieName.split(cookieItemDelimiter);
3024
+ var cookieValueParts = qq.getCookie(cookieName).split(cookieItemDelimiter);
3025
+
3026
+ resumableFilesData.push({
3027
+ name: decodeURIComponent(cookiesNameParts[1]),
3028
+ size: cookiesNameParts[2],
3029
+ uuid: cookieValueParts[0],
3030
+ partIdx: cookieValueParts[1]
3031
+ });
3032
+ });
3033
+
3034
+ return resumableFilesData;
3035
+ }
3036
+ return [];
3037
+ }
3038
+ };
3039
+
3040
+ return api;
3041
+ };
3042
+ /*globals jQuery, qq*/
3043
+ (function($) {
3044
+ "use strict";
3045
+ var uploader, $el, init, dataStore, pluginOption, pluginOptions, addCallbacks, transformVariables, isValidCommand,
3046
+ delegateCommand;
3047
+
3048
+ pluginOptions = ['uploaderType'];
3049
+
3050
+ init = function (options) {
3051
+ if (options) {
3052
+ var xformedOpts = transformVariables(options);
3053
+ addCallbacks(xformedOpts);
3054
+
3055
+ if (pluginOption('uploaderType') === 'basic') {
3056
+ uploader(new qq.FineUploaderBasic(xformedOpts));
3057
+ }
3058
+ else {
3059
+ uploader(new qq.FineUploader(xformedOpts));
3060
+ }
3061
+ }
3062
+
3063
+ return $el;
3064
+ };
3065
+
3066
+ dataStore = function(key, val) {
3067
+ var data = $el.data('fineuploader');
3068
+
3069
+ if (val) {
3070
+ if (data === undefined) {
3071
+ data = {};
3072
+ }
3073
+ data[key] = val;
3074
+ $el.data('fineuploader', data);
3075
+ }
3076
+ else {
3077
+ if (data === undefined) {
3078
+ return null;
3079
+ }
3080
+ return data[key];
3081
+ }
3082
+ };
3083
+
3084
+ //the underlying Fine Uploader instance is stored in jQuery's data stored, associated with the element
3085
+ // tied to this instance of the plug-in
3086
+ uploader = function(instanceToStore) {
3087
+ return dataStore('uploader', instanceToStore);
3088
+ };
3089
+
3090
+ pluginOption = function(option, optionVal) {
3091
+ return dataStore(option, optionVal);
3092
+ };
3093
+
3094
+ //implement all callbacks defined in Fine Uploader as functions that trigger appropriately names events and
3095
+ // return the result of executing the bound handler back to Fine Uploader
3096
+ addCallbacks = function(transformedOpts) {
3097
+ var callbacks = transformedOpts.callbacks = {};
3098
+
3099
+ $.each(new qq.FineUploaderBasic()._options.callbacks, function(prop, func) {
3100
+ var name, $callbackEl;
3101
+
3102
+ name = /^on(\w+)/.exec(prop)[1];
3103
+ name = name.substring(0, 1).toLowerCase() + name.substring(1);
3104
+ $callbackEl = $el;
3105
+
3106
+ callbacks[prop] = function() {
3107
+ var args = Array.prototype.slice.call(arguments);
3108
+ return $callbackEl.triggerHandler(name, args);
3109
+ };
3110
+ });
3111
+ };
3112
+
3113
+ //transform jQuery objects into HTMLElements, and pass along all other option properties
3114
+ transformVariables = function(source, dest) {
3115
+ var xformed, arrayVals;
3116
+
3117
+ if (dest === undefined) {
3118
+ if (source.uploaderType !== 'basic') {
3119
+ xformed = { element : $el[0] };
3120
+ }
3121
+ else {
3122
+ xformed = {};
3123
+ }
3124
+ }
3125
+ else {
3126
+ xformed = dest;
3127
+ }
3128
+
3129
+ $.each(source, function(prop, val) {
3130
+ if ($.inArray(prop, pluginOptions) >= 0) {
3131
+ pluginOption(prop, val);
3132
+ }
3133
+ else if (val instanceof $) {
3134
+ xformed[prop] = val[0];
3135
+ }
3136
+ else if ($.isPlainObject(val)) {
3137
+ xformed[prop] = {};
3138
+ transformVariables(val, xformed[prop]);
3139
+ }
3140
+ else if ($.isArray(val)) {
3141
+ arrayVals = [];
3142
+ $.each(val, function(idx, arrayVal) {
3143
+ if (arrayVal instanceof $) {
3144
+ $.merge(arrayVals, arrayVal);
3145
+ }
3146
+ else {
3147
+ arrayVals.push(arrayVal);
3148
+ }
3149
+ });
3150
+ xformed[prop] = arrayVals;
3151
+ }
3152
+ else {
3153
+ xformed[prop] = val;
3154
+ }
3155
+ });
3156
+
3157
+ if (dest === undefined) {
3158
+ return xformed;
3159
+ }
3160
+ };
3161
+
3162
+ isValidCommand = function(command) {
3163
+ return $.type(command) === "string" &&
3164
+ !command.match(/^_/) && //enforce private methods convention
3165
+ uploader()[command] !== undefined;
3166
+ };
3167
+
3168
+ //assuming we have already verified that this is a valid command, call the associated function in the underlying
3169
+ // Fine Uploader instance (passing along the arguments from the caller) and return the result of the call back to the caller
3170
+ delegateCommand = function(command) {
3171
+ var xformedArgs = [], origArgs = Array.prototype.slice.call(arguments, 1);
3172
+
3173
+ transformVariables(origArgs, xformedArgs);
3174
+
3175
+ return uploader()[command].apply(uploader(), xformedArgs);
3176
+ };
3177
+
3178
+ $.fn.fineUploader = function(optionsOrCommand) {
3179
+ var self = this, selfArgs = arguments, retVals = [];
3180
+
3181
+ this.each(function(index, el) {
3182
+ $el = $(el);
3183
+
3184
+ if (uploader() && isValidCommand(optionsOrCommand)) {
3185
+ retVals.push(delegateCommand.apply(self, selfArgs));
3186
+
3187
+ if (self.length === 1) {
3188
+ return false;
3189
+ }
3190
+ }
3191
+ else if (typeof optionsOrCommand === 'object' || !optionsOrCommand) {
3192
+ init.apply(self, selfArgs);
3193
+ }
3194
+ else {
3195
+ $.error('Method ' + optionsOrCommand + ' does not exist on jQuery.fineUploader');
3196
+ }
3197
+ });
3198
+
3199
+ if (retVals.length === 1) {
3200
+ return retVals[0];
3201
+ }
3202
+ else if (retVals.length > 1) {
3203
+ return retVals;
3204
+ }
3205
+
3206
+ return this;
3207
+ };
3208
+
3209
+ }(jQuery));