]> git.mxchange.org Git - friendica-addons.git/blob - js_upload/file-uploader/client/fileuploader.js
some files were executable, now they are not
[friendica-addons.git] / js_upload / file-uploader / client / fileuploader.js
1 /**
2  * http://github.com/valums/file-uploader
3  * 
4  * Multiple file upload component with progress-bar, drag-and-drop. 
5  * © 2010 Andrew Valums ( andrew(at)valums.com ) 
6  * 
7  * Licensed under GNU GPL 2 or later, see license.txt.
8  */    
9
10 //
11 // Helper functions
12 //
13
14 var qq = qq || {};
15
16 /**
17  * Adds all missing properties from second obj to first obj
18  */ 
19 qq.extend = function(first, second){
20     for (var prop in second){
21         first[prop] = second[prop];
22     }
23 };  
24
25 /**
26  * Searches for a given element in the array, returns -1 if it is not present.
27  * @param {Number} [from] The index at which to begin the search
28  */
29 qq.indexOf = function(arr, elt, from){
30     if (arr.indexOf) return arr.indexOf(elt, from);
31     
32     from = from || 0;
33     var len = arr.length;    
34     
35     if (from < 0) from += len;  
36
37     for (; from < len; from++){  
38         if (from in arr && arr[from] === elt){  
39             return from;
40         }
41     }  
42     return -1;  
43 }; 
44     
45 qq.getUniqueId = (function(){
46     var id = 0;
47     return function(){ return id++; };
48 })();
49
50 //
51 // Events
52
53 qq.attach = function(element, type, fn){
54     if (element.addEventListener){
55         element.addEventListener(type, fn, false);
56     } else if (element.attachEvent){
57         element.attachEvent('on' + type, fn);
58     }
59 };
60 qq.detach = function(element, type, fn){
61     if (element.removeEventListener){
62         element.removeEventListener(type, fn, false);
63     } else if (element.attachEvent){
64         element.detachEvent('on' + type, fn);
65     }
66 };
67
68 qq.preventDefault = function(e){
69     if (e.preventDefault){
70         e.preventDefault();
71     } else{
72         e.returnValue = false;
73     }
74 };
75
76 //
77 // Node manipulations
78
79 /**
80  * Insert node a before node b.
81  */
82 qq.insertBefore = function(a, b){
83     b.parentNode.insertBefore(a, b);
84 };
85 qq.remove = function(element){
86     element.parentNode.removeChild(element);
87 };
88
89 qq.contains = function(parent, descendant){       
90     // compareposition returns false in this case
91     if (parent == descendant) return true;
92     
93     if (parent.contains){
94         return parent.contains(descendant);
95     } else {
96         return !!(descendant.compareDocumentPosition(parent) & 8);
97     }
98 };
99
100 /**
101  * Creates and returns element from html string
102  * Uses innerHTML to create an element
103  */
104 qq.toElement = (function(){
105     var div = document.createElement('div');
106     return function(html){
107         div.innerHTML = html;
108         var element = div.firstChild;
109         div.removeChild(element);
110         return element;
111     };
112 })();
113
114 //
115 // Node properties and attributes
116
117 /**
118  * Sets styles for an element.
119  * Fixes opacity in IE6-8.
120  */
121 qq.css = function(element, styles){
122     if (styles.opacity != null){
123         if (typeof element.style.opacity != 'string' && typeof(element.filters) != 'undefined'){
124             styles.filter = 'alpha(opacity=' + Math.round(100 * styles.opacity) + ')';
125         }
126     }
127     qq.extend(element.style, styles);
128 };
129 qq.hasClass = function(element, name){
130     var re = new RegExp('(^| )' + name + '( |$)');
131     return re.test(element.className);
132 };
133 qq.addClass = function(element, name){
134     if (!qq.hasClass(element, name)){
135         element.className += ' ' + name;
136     }
137 };
138 qq.removeClass = function(element, name){
139     var re = new RegExp('(^| )' + name + '( |$)');
140     element.className = element.className.replace(re, ' ').replace(/^\s+|\s+$/g, "");
141 };
142 qq.setText = function(element, text){
143     element.innerText = text;
144     element.textContent = text;
145 };
146
147 //
148 // Selecting elements
149
150 qq.children = function(element){
151     var children = [],
152     child = element.firstChild;
153
154     while (child){
155         if (child.nodeType == 1){
156             children.push(child);
157         }
158         child = child.nextSibling;
159     }
160
161     return children;
162 };
163
164 qq.getByClass = function(element, className){
165     if (element.querySelectorAll){
166         return element.querySelectorAll('.' + className);
167     }
168
169     var result = [];
170     var candidates = element.getElementsByTagName("*");
171     var len = candidates.length;
172
173     for (var i = 0; i < len; i++){
174         if (qq.hasClass(candidates[i], className)){
175             result.push(candidates[i]);
176         }
177     }
178     return result;
179 };
180
181 /**
182  * obj2url() takes a json-object as argument and generates
183  * a querystring. pretty much like jQuery.param()
184  * 
185  * how to use:
186  *
187  *    `qq.obj2url({a:'b',c:'d'},'http://any.url/upload?otherParam=value');`
188  *
189  * will result in:
190  *
191  *    `http://any.url/upload?otherParam=value&a=b&c=d`
192  *
193  * @param  Object JSON-Object
194  * @param  String current querystring-part
195  * @return String encoded querystring
196  */
197 qq.obj2url = function(obj, temp, prefixDone){
198     var uristrings = [],
199         prefix = '&',
200         add = function(nextObj, i){
201             var nextTemp = temp 
202                 ? (/\[\]$/.test(temp)) // prevent double-encoding
203                    ? temp
204                    : temp+'['+i+']'
205                 : i;
206             if ((nextTemp != 'undefined') && (i != 'undefined')) {  
207                 uristrings.push(
208                     (typeof nextObj === 'object') 
209                         ? qq.obj2url(nextObj, nextTemp, true)
210                         : (Object.prototype.toString.call(nextObj) === '[object Function]')
211                             ? encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj())
212                             : encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj)                                                          
213                 );
214             }
215         }; 
216
217     if (!prefixDone && temp) {
218       prefix = (/\?/.test(temp)) ? (/\?$/.test(temp)) ? '' : '&' : '?';
219       uristrings.push(temp);
220       uristrings.push(qq.obj2url(obj));
221     } else if ((Object.prototype.toString.call(obj) === '[object Array]') && (typeof obj != 'undefined') ) {
222         // we wont use a for-in-loop on an array (performance)
223         for (var i = 0, len = obj.length; i < len; ++i){
224             add(obj[i], i);
225         }
226     } else if ((typeof obj != 'undefined') && (obj !== null) && (typeof obj === "object")){
227         // for anything else but a scalar, we will use for-in-loop
228         for (var i in obj){
229             add(obj[i], i);
230         }
231     } else {
232         uristrings.push(encodeURIComponent(temp) + '=' + encodeURIComponent(obj));
233     }
234
235     return uristrings.join(prefix)
236                      .replace(/^&/, '')
237                      .replace(/%20/g, '+'); 
238 };
239
240 //
241 //
242 // Uploader Classes
243 //
244 //
245
246 var qq = qq || {};
247     
248 /**
249  * Creates upload button, validates upload, but doesn't create file list or dd. 
250  */
251 qq.FileUploaderBasic = function(o){
252     this._options = {
253         // set to true to see the server response
254         debug: false,
255         action: '/server/upload',
256         params: {},
257         button: null,
258         multiple: true,
259         maxConnections: 3,
260         // validation        
261         allowedExtensions: [],               
262         sizeLimit: 0,   
263         minSizeLimit: 0,                             
264         // events
265         // return false to cancel submit
266         onSubmit: function(id, fileName){},
267         onProgress: function(id, fileName, loaded, total){},
268         onComplete: function(id, fileName, responseJSON){},
269         onCancel: function(id, fileName){},
270         // messages                
271         messages: {
272             typeError: "{file} has invalid extension. Only {extensions} are allowed.",
273             sizeError: "{file} is too large, maximum file size is {sizeLimit}.",
274             minSizeError: "{file} is too small, minimum file size is {minSizeLimit}.",
275             emptyError: "{file} is empty, please select files again without it.",
276             onLeave: "The files are being uploaded, if you leave now the upload will be cancelled."            
277         },
278         showMessage: function(message){
279             alert(message);
280         }               
281     };
282     qq.extend(this._options, o);
283         
284     // number of files being uploaded
285     this._filesInProgress = 0;
286     this._handler = this._createUploadHandler(); 
287     
288     if (this._options.button){ 
289         this._button = this._createUploadButton(this._options.button);
290     }
291                         
292     this._preventLeaveInProgress();         
293 };
294    
295 qq.FileUploaderBasic.prototype = {
296     setParams: function(params){
297         this._options.params = params;
298     },
299     getInProgress: function(){
300         return this._filesInProgress;         
301     },
302     _createUploadButton: function(element){
303         var self = this;
304         
305         return new qq.UploadButton({
306             element: element,
307             multiple: this._options.multiple && qq.UploadHandlerXhr.isSupported(),
308             onChange: function(input){
309                 self._onInputChange(input);
310             }        
311         });           
312     },    
313     _createUploadHandler: function(){
314         var self = this,
315             handlerClass;        
316         
317         if(qq.UploadHandlerXhr.isSupported()){           
318             handlerClass = 'UploadHandlerXhr';                        
319         } else {
320             handlerClass = 'UploadHandlerForm';
321         }
322
323         var handler = new qq[handlerClass]({
324             debug: this._options.debug,
325             action: this._options.action,         
326             maxConnections: this._options.maxConnections,   
327             onProgress: function(id, fileName, loaded, total){                
328                 self._onProgress(id, fileName, loaded, total);
329                 self._options.onProgress(id, fileName, loaded, total);                    
330             },            
331             onComplete: function(id, fileName, result){
332                 self._onComplete(id, fileName, result);
333                 self._options.onComplete(id, fileName, result);
334             },
335             onCancel: function(id, fileName){
336                 self._onCancel(id, fileName);
337                 self._options.onCancel(id, fileName);
338             }
339         });
340
341         return handler;
342     },    
343     _preventLeaveInProgress: function(){
344         var self = this;
345         
346         qq.attach(window, 'beforeunload', function(e){
347             if (!self._filesInProgress){return;}
348             
349             var e = e || window.event;
350             // for ie, ff
351             e.returnValue = self._options.messages.onLeave;
352             // for webkit
353             return self._options.messages.onLeave;             
354         });        
355     },    
356     _onSubmit: function(id, fileName){
357         this._filesInProgress++;  
358     },
359     _onProgress: function(id, fileName, loaded, total){        
360     },
361     _onComplete: function(id, fileName, result){
362         this._filesInProgress--;                 
363         if (result.error){
364             this._options.showMessage(result.error);
365         }             
366     },
367     _onCancel: function(id, fileName){
368         this._filesInProgress--;        
369     },
370     _onInputChange: function(input){
371         if (this._handler instanceof qq.UploadHandlerXhr){                
372             this._uploadFileList(input.files);                   
373         } else {             
374             if (this._validateFile(input)){                
375                 this._uploadFile(input);                                    
376             }                      
377         }               
378         this._button.reset();   
379     },  
380     _uploadFileList: function(files){
381         for (var i=0; i<files.length; i++){
382             if ( !this._validateFile(files[i])){
383                 return;
384             }            
385         }
386         
387         for (var i=0; i<files.length; i++){
388             this._uploadFile(files[i]);        
389         }        
390     },       
391     _uploadFile: function(fileContainer){      
392         var id = this._handler.add(fileContainer);
393         var fileName = this._handler.getName(id);
394         
395         if (this._options.onSubmit(id, fileName) !== false){
396             this._onSubmit(id, fileName);
397             this._handler.upload(id, this._options.params);
398         }
399     },      
400     _validateFile: function(file){
401         var name, size;
402         
403         if (file.value){
404             // it is a file input            
405             // get input value and remove path to normalize
406             name = file.value.replace(/.*(\/|\\)/, "");
407         } else {
408             // fix missing properties in Safari
409             name = file.fileName != null ? file.fileName : file.name;
410             size = file.fileSize != null ? file.fileSize : file.size;
411         }
412                     
413         if (! this._isAllowedExtension(name)){            
414             this._error('typeError', name);
415             return false;
416             
417         } else if (size === 0){            
418             this._error('emptyError', name);
419             return false;
420                                                      
421         } else if (size && this._options.sizeLimit && size > this._options.sizeLimit){            
422             this._error('sizeError', name);
423             return false;
424                         
425         } else if (size && size < this._options.minSizeLimit){
426             this._error('minSizeError', name);
427             return false;            
428         }
429         
430         return true;                
431     },
432     _error: function(code, fileName){
433         var message = this._options.messages[code];        
434         function r(name, replacement){ message = message.replace(name, replacement); }
435         
436         r('{file}', this._formatFileName(fileName));        
437         r('{extensions}', this._options.allowedExtensions.join(', '));
438         r('{sizeLimit}', this._formatSize(this._options.sizeLimit));
439         r('{minSizeLimit}', this._formatSize(this._options.minSizeLimit));
440         
441         this._options.showMessage(message);                
442     },
443     _formatFileName: function(name){
444         if (name.length > 33){
445             name = name.slice(0, 19) + '...' + name.slice(-13);    
446         }
447         return name;
448     },
449     _isAllowedExtension: function(fileName){
450         var ext = (-1 !== fileName.indexOf('.')) ? fileName.replace(/.*[.]/, '').toLowerCase() : '';
451         var allowed = this._options.allowedExtensions;
452         
453         if (!allowed.length){return true;}        
454         
455         for (var i=0; i<allowed.length; i++){
456             if (allowed[i].toLowerCase() == ext){ return true;}    
457         }
458         
459         return false;
460     },    
461     _formatSize: function(bytes){
462         var i = -1;                                    
463         do {
464             bytes = bytes / 1024;
465             i++;  
466         } while (bytes > 99);
467         
468         return Math.max(bytes, 0.1).toFixed(1) + ['kB', 'MB', 'GB', 'TB', 'PB', 'EB'][i];          
469     }
470 };
471     
472        
473 /**
474  * Class that creates upload widget with drag-and-drop and file list
475  * @inherits qq.FileUploaderBasic
476  */
477 qq.FileUploader = function(o){
478     // call parent constructor
479     qq.FileUploaderBasic.apply(this, arguments);
480     
481     // additional options    
482     qq.extend(this._options, {
483         element: null,
484         // if set, will be used instead of qq-upload-list in template
485         listElement: null,
486                 
487         template: '<div class="qq-uploader">' + 
488                 '<div class="qq-upload-drop-area"><span>Drop files here to upload</span></div>' +
489                 '<div class="qq-upload-button">Upload a file</div>' +
490                 '<ul class="qq-upload-list"></ul>' + 
491              '</div>',
492
493         // template for one item in file list
494         fileTemplate: '<li>' +
495                 '<span class="qq-upload-file"></span>' +
496                 '<span class="qq-upload-spinner"></span>' +
497                 '<span class="qq-upload-size"></span>' +
498                 '<a class="qq-upload-cancel" href="#">Cancel</a>' +
499                 '<span class="qq-upload-failed-text">Failed</span>' +
500             '</li>',        
501         
502         classes: {
503             // used to get elements from templates
504             button: 'qq-upload-button',
505             drop: 'qq-upload-drop-area',
506             dropActive: 'qq-upload-drop-area-active',
507             list: 'qq-upload-list',
508                         
509             file: 'qq-upload-file',
510             spinner: 'qq-upload-spinner',
511             size: 'qq-upload-size',
512             cancel: 'qq-upload-cancel',
513
514             // added to list item when upload completes
515             // used in css to hide progress spinner
516             success: 'qq-upload-success',
517             fail: 'qq-upload-fail'
518         }
519     });
520     // overwrite options with user supplied    
521     qq.extend(this._options, o);       
522
523     this._element = this._options.element;
524     this._element.innerHTML = this._options.template;        
525     this._listElement = this._options.listElement || this._find(this._element, 'list');
526     
527     this._classes = this._options.classes;
528         
529     this._button = this._createUploadButton(this._find(this._element, 'button'));        
530     
531     this._bindCancelEvent();
532     this._setupDragDrop();
533 };
534
535 // inherit from Basic Uploader
536 qq.extend(qq.FileUploader.prototype, qq.FileUploaderBasic.prototype);
537
538 qq.extend(qq.FileUploader.prototype, {
539     /**
540      * Gets one of the elements listed in this._options.classes
541      **/
542     _find: function(parent, type){                                
543         var element = qq.getByClass(parent, this._options.classes[type])[0];        
544         if (!element){
545             throw new Error('element not found ' + type);
546         }
547         
548         return element;
549     },
550     _setupDragDrop: function(){
551         var self = this,
552             dropArea = this._find(this._element, 'drop');                        
553
554         var dz = new qq.UploadDropZone({
555             element: dropArea,
556             onEnter: function(e){
557                 qq.addClass(dropArea, self._classes.dropActive);
558                 e.stopPropagation();
559             },
560             onLeave: function(e){
561                 e.stopPropagation();
562             },
563             onLeaveNotDescendants: function(e){
564                 qq.removeClass(dropArea, self._classes.dropActive);  
565             },
566             onDrop: function(e){
567                 dropArea.style.display = 'none';
568                 qq.removeClass(dropArea, self._classes.dropActive);
569                 self._uploadFileList(e.dataTransfer.files);    
570             }
571         });
572                 
573         dropArea.style.display = 'none';
574
575         qq.attach(document, 'dragenter', function(e){     
576             if (!dz._isValidFileDrag(e)) return; 
577             
578             dropArea.style.display = 'block';            
579         });                 
580         qq.attach(document, 'dragleave', function(e){
581             if (!dz._isValidFileDrag(e)) return;            
582             
583             var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);
584             // only fire when leaving document out
585             if ( ! relatedTarget || relatedTarget.nodeName == "HTML"){               
586                 dropArea.style.display = 'none';                                            
587             }
588         });                
589     },
590     _onSubmit: function(id, fileName){
591         qq.FileUploaderBasic.prototype._onSubmit.apply(this, arguments);
592         this._addToList(id, fileName);  
593     },
594     _onProgress: function(id, fileName, loaded, total){
595         qq.FileUploaderBasic.prototype._onProgress.apply(this, arguments);
596
597         var item = this._getItemByFileId(id);
598         var size = this._find(item, 'size');
599         size.style.display = 'inline';
600         
601         var text; 
602         if (loaded != total){
603             text = Math.round(loaded / total * 100) + '% from ' + this._formatSize(total);
604         } else {                                   
605             text = this._formatSize(total);
606         }          
607         
608         qq.setText(size, text);         
609     },
610     _onComplete: function(id, fileName, result){
611         qq.FileUploaderBasic.prototype._onComplete.apply(this, arguments);
612
613         // mark completed
614         var item = this._getItemByFileId(id);                
615         qq.remove(this._find(item, 'cancel'));
616         qq.remove(this._find(item, 'spinner'));
617         
618         if (result.success){
619             qq.addClass(item, this._classes.success);    
620         } else {
621             qq.addClass(item, this._classes.fail);
622         }         
623     },
624     _addToList: function(id, fileName){
625         var item = qq.toElement(this._options.fileTemplate);                
626         item.qqFileId = id;
627
628         var fileElement = this._find(item, 'file');        
629         qq.setText(fileElement, this._formatFileName(fileName));
630         this._find(item, 'size').style.display = 'none';        
631
632         this._listElement.appendChild(item);
633     },
634     _getItemByFileId: function(id){
635         var item = this._listElement.firstChild;        
636         
637         // there can't be txt nodes in dynamically created list
638         // and we can  use nextSibling
639         while (item){            
640             if (item.qqFileId == id) return item;            
641             item = item.nextSibling;
642         }          
643     },
644     /**
645      * delegate click event for cancel link 
646      **/
647     _bindCancelEvent: function(){
648         var self = this,
649             list = this._listElement;            
650         
651         qq.attach(list, 'click', function(e){            
652             e = e || window.event;
653             var target = e.target || e.srcElement;
654             
655             if (qq.hasClass(target, self._classes.cancel)){                
656                 qq.preventDefault(e);
657                
658                 var item = target.parentNode;
659                 self._handler.cancel(item.qqFileId);
660                 qq.remove(item);
661             }
662         });
663     }    
664 });
665     
666 qq.UploadDropZone = function(o){
667     this._options = {
668         element: null,  
669         onEnter: function(e){},
670         onLeave: function(e){},  
671         // is not fired when leaving element by hovering descendants   
672         onLeaveNotDescendants: function(e){},   
673         onDrop: function(e){}                       
674     };
675     qq.extend(this._options, o); 
676     
677     this._element = this._options.element;
678     
679     this._disableDropOutside();
680     this._attachEvents();   
681 };
682
683 qq.UploadDropZone.prototype = {
684     _disableDropOutside: function(e){
685         // run only once for all instances
686         if (!qq.UploadDropZone.dropOutsideDisabled ){
687
688             qq.attach(document, 'dragover', function(e){
689                 if (e.dataTransfer){
690                     e.dataTransfer.dropEffect = 'none';
691                     e.preventDefault(); 
692                 }           
693             });
694             
695             qq.UploadDropZone.dropOutsideDisabled = true; 
696         }        
697     },
698     _attachEvents: function(){
699         var self = this;              
700                   
701         qq.attach(self._element, 'dragover', function(e){
702             if (!self._isValidFileDrag(e)) return;
703             
704             var effect = e.dataTransfer.effectAllowed;
705             if (effect == 'move' || effect == 'linkMove'){
706                 e.dataTransfer.dropEffect = 'move'; // for FF (only move allowed)    
707             } else {                    
708                 e.dataTransfer.dropEffect = 'copy'; // for Chrome
709             }
710                                                      
711             e.stopPropagation();
712             e.preventDefault();                                                                    
713         });
714         
715         qq.attach(self._element, 'dragenter', function(e){
716             if (!self._isValidFileDrag(e)) return;
717                         
718             self._options.onEnter(e);
719         });
720         
721         qq.attach(self._element, 'dragleave', function(e){
722             if (!self._isValidFileDrag(e)) return;
723             
724             self._options.onLeave(e);
725             
726             var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);                      
727             // do not fire when moving a mouse over a descendant
728             if (qq.contains(this, relatedTarget)) return;
729                         
730             self._options.onLeaveNotDescendants(e); 
731         });
732                 
733         qq.attach(self._element, 'drop', function(e){
734             if (!self._isValidFileDrag(e)) return;
735             
736             e.preventDefault();
737             self._options.onDrop(e);
738         });          
739     },
740     _isValidFileDrag: function(e){
741         var dt = e.dataTransfer,
742             // do not check dt.types.contains in webkit, because it crashes safari 4            
743             isWebkit = navigator.userAgent.indexOf("AppleWebKit") > -1;                        
744
745         // dt.effectAllowed is none in Safari 5
746         // dt.types.contains check is for firefox            
747         return dt && dt.effectAllowed != 'none' && 
748             (dt.files || (!isWebkit && dt.types.contains && dt.types.contains('Files')));
749         
750     }        
751 }; 
752
753 qq.UploadButton = function(o){
754     this._options = {
755         element: null,  
756         // if set to true adds multiple attribute to file input      
757         multiple: false,
758         // name attribute of file input
759         name: 'file',
760         onChange: function(input){},
761         hoverClass: 'qq-upload-button-hover',
762         focusClass: 'qq-upload-button-focus'                       
763     };
764     
765     qq.extend(this._options, o);
766         
767     this._element = this._options.element;
768     
769     // make button suitable container for input
770     qq.css(this._element, {
771         position: 'relative',
772         overflow: 'hidden',
773         // Make sure browse button is in the right side
774         // in Internet Explorer
775         direction: 'ltr'
776     });   
777     
778     this._input = this._createInput();
779 };
780
781 qq.UploadButton.prototype = {
782     /* returns file input element */    
783     getInput: function(){
784         return this._input;
785     },
786     /* cleans/recreates the file input */
787     reset: function(){
788         if (this._input.parentNode){
789             qq.remove(this._input);    
790         }                
791         
792         qq.removeClass(this._element, this._options.focusClass);
793         this._input = this._createInput();
794     },    
795     _createInput: function(){                
796         var input = document.createElement("input");
797         
798         if (this._options.multiple){
799             input.setAttribute("multiple", "multiple");
800         }
801                 
802         input.setAttribute("type", "file");
803         input.setAttribute("name", this._options.name);
804         
805         qq.css(input, {
806             position: 'absolute',
807             // in Opera only 'browse' button
808             // is clickable and it is located at
809             // the right side of the input
810             right: 0,
811             top: 0,
812             fontFamily: 'Arial',
813             // 4 persons reported this, the max values that worked for them were 243, 236, 236, 118
814             fontSize: '118px',
815             margin: 0,
816             padding: 0,
817             cursor: 'pointer',
818             opacity: 0
819         });
820         
821         this._element.appendChild(input);
822
823         var self = this;
824         qq.attach(input, 'change', function(){
825             self._options.onChange(input);
826         });
827                 
828         qq.attach(input, 'mouseover', function(){
829             qq.addClass(self._element, self._options.hoverClass);
830         });
831         qq.attach(input, 'mouseout', function(){
832             qq.removeClass(self._element, self._options.hoverClass);
833         });
834         qq.attach(input, 'focus', function(){
835             qq.addClass(self._element, self._options.focusClass);
836         });
837         qq.attach(input, 'blur', function(){
838             qq.removeClass(self._element, self._options.focusClass);
839         });
840
841         // IE and Opera, unfortunately have 2 tab stops on file input
842         // which is unacceptable in our case, disable keyboard access
843         if (window.attachEvent){
844             // it is IE or Opera
845             input.setAttribute('tabIndex', "-1");
846         }
847
848         return input;            
849     }        
850 };
851
852 /**
853  * Class for uploading files, uploading itself is handled by child classes
854  */
855 qq.UploadHandlerAbstract = function(o){
856     this._options = {
857         debug: false,
858         action: '/upload.php',
859         // maximum number of concurrent uploads        
860         maxConnections: 999,
861         onProgress: function(id, fileName, loaded, total){},
862         onComplete: function(id, fileName, response){},
863         onCancel: function(id, fileName){}
864     };
865     qq.extend(this._options, o);    
866     
867     this._queue = [];
868     // params for files in queue
869     this._params = [];
870 };
871 qq.UploadHandlerAbstract.prototype = {
872     log: function(str){
873         if (this._options.debug && window.console) console.log('[uploader] ' + str);        
874     },
875     /**
876      * Adds file or file input to the queue
877      * @returns id
878      **/    
879     add: function(file){},
880     /**
881      * Sends the file identified by id and additional query params to the server
882      */
883     upload: function(id, params){
884         var len = this._queue.push(id);
885
886         var copy = {};        
887         qq.extend(copy, params);
888         this._params[id] = copy;        
889                 
890         // if too many active uploads, wait...
891         if (len <= this._options.maxConnections){               
892             this._upload(id, this._params[id]);
893         }
894     },
895     /**
896      * Cancels file upload by id
897      */
898     cancel: function(id){
899         this._cancel(id);
900         this._dequeue(id);
901     },
902     /**
903      * Cancells all uploads
904      */
905     cancelAll: function(){
906         for (var i=0; i<this._queue.length; i++){
907             this._cancel(this._queue[i]);
908         }
909         this._queue = [];
910     },
911     /**
912      * Returns name of the file identified by id
913      */
914     getName: function(id){},
915     /**
916      * Returns size of the file identified by id
917      */          
918     getSize: function(id){},
919     /**
920      * Returns id of files being uploaded or
921      * waiting for their turn
922      */
923     getQueue: function(){
924         return this._queue;
925     },
926     /**
927      * Actual upload method
928      */
929     _upload: function(id){},
930     /**
931      * Actual cancel method
932      */
933     _cancel: function(id){},     
934     /**
935      * Removes element from queue, starts upload of next
936      */
937     _dequeue: function(id){
938         var i = qq.indexOf(this._queue, id);
939         this._queue.splice(i, 1);
940                 
941         var max = this._options.maxConnections;
942         
943         if (this._queue.length >= max){
944             var nextId = this._queue[max-1];
945             this._upload(nextId, this._params[nextId]);
946         }
947     }        
948 };
949
950 /**
951  * Class for uploading files using form and iframe
952  * @inherits qq.UploadHandlerAbstract
953  */
954 qq.UploadHandlerForm = function(o){
955     qq.UploadHandlerAbstract.apply(this, arguments);
956        
957     this._inputs = {};
958 };
959 // @inherits qq.UploadHandlerAbstract
960 qq.extend(qq.UploadHandlerForm.prototype, qq.UploadHandlerAbstract.prototype);
961
962 qq.extend(qq.UploadHandlerForm.prototype, {
963     add: function(fileInput){
964         fileInput.setAttribute('name', 'qqfile');
965         var id = 'qq-upload-handler-iframe' + qq.getUniqueId();       
966         
967         this._inputs[id] = fileInput;
968         
969         // remove file input from DOM
970         if (fileInput.parentNode){
971             qq.remove(fileInput);
972         }
973                 
974         return id;
975     },
976     getName: function(id){
977         // get input value and remove path to normalize
978         return this._inputs[id].value.replace(/.*(\/|\\)/, "");
979     },    
980     _cancel: function(id){
981         this._options.onCancel(id, this.getName(id));
982         
983         delete this._inputs[id];        
984
985         var iframe = document.getElementById(id);
986         if (iframe){
987             // to cancel request set src to something else
988             // we use src="javascript:false;" because it doesn't
989             // trigger ie6 prompt on https
990             iframe.setAttribute('src', 'javascript:false;');
991
992             qq.remove(iframe);
993         }
994     },     
995     _upload: function(id, params){                        
996         var input = this._inputs[id];
997         
998         if (!input){
999             throw new Error('file with passed id was not added, or already uploaded or cancelled');
1000         }                
1001
1002         var fileName = this.getName(id);
1003                 
1004         var iframe = this._createIframe(id);
1005         var form = this._createForm(iframe, params);
1006         form.appendChild(input);
1007
1008         var self = this;
1009         this._attachLoadEvent(iframe, function(){                                 
1010             self.log('iframe loaded');
1011             
1012             var response = self._getIframeContentJSON(iframe);
1013
1014             self._options.onComplete(id, fileName, response);
1015             self._dequeue(id);
1016             
1017             delete self._inputs[id];
1018             // timeout added to fix busy state in FF3.6
1019             setTimeout(function(){
1020                 qq.remove(iframe);
1021             }, 1);
1022         });
1023
1024         form.submit();        
1025         qq.remove(form);        
1026         
1027         return id;
1028     }, 
1029     _attachLoadEvent: function(iframe, callback){
1030         qq.attach(iframe, 'load', function(){
1031             // when we remove iframe from dom
1032             // the request stops, but in IE load
1033             // event fires
1034             if (!iframe.parentNode){
1035                 return;
1036             }
1037
1038             // fixing Opera 10.53
1039             if (iframe.contentDocument &&
1040                 iframe.contentDocument.body &&
1041                 iframe.contentDocument.body.innerHTML == "false"){
1042                 // In Opera event is fired second time
1043                 // when body.innerHTML changed from false
1044                 // to server response approx. after 1 sec
1045                 // when we upload file with iframe
1046                 return;
1047             }
1048
1049             callback();
1050         });
1051     },
1052     /**
1053      * Returns json object received by iframe from server.
1054      */
1055     _getIframeContentJSON: function(iframe){
1056         // iframe.contentWindow.document - for IE<7
1057         var doc = iframe.contentDocument ? iframe.contentDocument: iframe.contentWindow.document,
1058             response;
1059         
1060         this.log("converting iframe's innerHTML to JSON");
1061         this.log("innerHTML = " + doc.body.innerHTML);
1062                         
1063         try {
1064             response = eval("(" + doc.body.innerHTML + ")");
1065         } catch(err){
1066             response = {};
1067         }        
1068
1069         return response;
1070     },
1071     /**
1072      * Creates iframe with unique name
1073      */
1074     _createIframe: function(id){
1075         // We can't use following code as the name attribute
1076         // won't be properly registered in IE6, and new window
1077         // on form submit will open
1078         // var iframe = document.createElement('iframe');
1079         // iframe.setAttribute('name', id);
1080
1081         var iframe = qq.toElement('<iframe src="javascript:false;" name="' + id + '" />');
1082         // src="javascript:false;" removes ie6 prompt on https
1083
1084         iframe.setAttribute('id', id);
1085
1086         iframe.style.display = 'none';
1087         document.body.appendChild(iframe);
1088
1089         return iframe;
1090     },
1091     /**
1092      * Creates form, that will be submitted to iframe
1093      */
1094     _createForm: function(iframe, params){
1095         // We can't use the following code in IE6
1096         // var form = document.createElement('form');
1097         // form.setAttribute('method', 'post');
1098         // form.setAttribute('enctype', 'multipart/form-data');
1099         // Because in this case file won't be attached to request
1100         var form = qq.toElement('<form method="post" enctype="multipart/form-data"></form>');
1101
1102         var queryString = qq.obj2url(params, this._options.action);
1103
1104         form.setAttribute('action', queryString);
1105         form.setAttribute('target', iframe.name);
1106         form.style.display = 'none';
1107         document.body.appendChild(form);
1108
1109         return form;
1110     }
1111 });
1112
1113 /**
1114  * Class for uploading files using xhr
1115  * @inherits qq.UploadHandlerAbstract
1116  */
1117 qq.UploadHandlerXhr = function(o){
1118     qq.UploadHandlerAbstract.apply(this, arguments);
1119
1120     this._files = [];
1121     this._xhrs = [];
1122     
1123     // current loaded size in bytes for each file 
1124     this._loaded = [];
1125 };
1126
1127 // static method
1128 qq.UploadHandlerXhr.isSupported = function(){
1129     var input = document.createElement('input');
1130     input.type = 'file';        
1131     
1132     return (
1133         'multiple' in input &&
1134         typeof File != "undefined" &&
1135         typeof (new XMLHttpRequest()).upload != "undefined" );       
1136 };
1137
1138 // @inherits qq.UploadHandlerAbstract
1139 qq.extend(qq.UploadHandlerXhr.prototype, qq.UploadHandlerAbstract.prototype)
1140
1141 qq.extend(qq.UploadHandlerXhr.prototype, {
1142     /**
1143      * Adds file to the queue
1144      * Returns id to use with upload, cancel
1145      **/    
1146     add: function(file){
1147         if (!(file instanceof File)){
1148             throw new Error('Passed obj in not a File (in qq.UploadHandlerXhr)');
1149         }
1150                 
1151         return this._files.push(file) - 1;        
1152     },
1153     getName: function(id){        
1154         var file = this._files[id];
1155         // fix missing name in Safari 4
1156         return file.fileName != null ? file.fileName : file.name;       
1157     },
1158     getSize: function(id){
1159         var file = this._files[id];
1160         return file.fileSize != null ? file.fileSize : file.size;
1161     },    
1162     /**
1163      * Returns uploaded bytes for file identified by id 
1164      */    
1165     getLoaded: function(id){
1166         return this._loaded[id] || 0; 
1167     },
1168     /**
1169      * Sends the file identified by id and additional query params to the server
1170      * @param {Object} params name-value string pairs
1171      */    
1172     _upload: function(id, params){
1173         var file = this._files[id],
1174             name = this.getName(id),
1175             size = this.getSize(id);
1176                 
1177         this._loaded[id] = 0;
1178                                 
1179         var xhr = this._xhrs[id] = new XMLHttpRequest();
1180         var self = this;
1181                                         
1182         xhr.upload.onprogress = function(e){
1183             if (e.lengthComputable){
1184                 self._loaded[id] = e.loaded;
1185                 self._options.onProgress(id, name, e.loaded, e.total);
1186             }
1187         };
1188
1189         xhr.onreadystatechange = function(){            
1190             if (xhr.readyState == 4){
1191                 self._onComplete(id, xhr);                    
1192             }
1193         };
1194
1195         // build query string
1196         params = params || {};
1197         params['qqfile'] = name;
1198         var queryString = qq.obj2url(params, this._options.action);
1199
1200         xhr.open("POST", queryString, true);
1201         xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
1202         xhr.setRequestHeader("X-File-Name", encodeURIComponent(name));
1203         xhr.setRequestHeader("Content-Type", "application/octet-stream");
1204         xhr.send(file);
1205     },
1206     _onComplete: function(id, xhr){
1207         // the request was aborted/cancelled
1208         if (!this._files[id]) return;
1209         
1210         var name = this.getName(id);
1211         var size = this.getSize(id);
1212         
1213         this._options.onProgress(id, name, size, size);
1214                 
1215         if (xhr.status == 200){
1216             this.log("xhr - server response received");
1217             this.log("responseText = " + xhr.responseText);
1218                         
1219             var response;
1220                     
1221             try {
1222                 response = eval("(" + xhr.responseText + ")");
1223             } catch(err){
1224                 response = {};
1225             }
1226             
1227             this._options.onComplete(id, name, response);
1228                         
1229         } else {                   
1230             this._options.onComplete(id, name, {});
1231         }
1232                 
1233         this._files[id] = null;
1234         this._xhrs[id] = null;    
1235         this._dequeue(id);                    
1236     },
1237     _cancel: function(id){
1238         this._options.onCancel(id, this.getName(id));
1239         
1240         this._files[id] = null;
1241         
1242         if (this._xhrs[id]){
1243             this._xhrs[id].abort();
1244             this._xhrs[id] = null;                                   
1245         }
1246     }
1247 });