fine_uploader 2.1.1

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