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