2012年2月13日 星期一

ASP.NET MVC上傳檔案,使用file-uploader : 進階操作 Part.3


這個系列的文章之前有介紹過了基本操作與幾種進階操作的使用方式,本篇文章將會介紹以下內容後來作為收尾,

  • 設定是否可以多選檔案上傳
  • 設定是否可以拖曳檔案?!
  • 修改拖曳時可以上傳一個檔案而不可上傳多個檔案


    P.S.
    1.後兩個項目的進階操作會去修改到file-uploader.js的原始檔。
    2.此三項功能的修改主要是針對非IE瀏覽器。


  • 設定是否可以多選檔案上傳

    在 valums file-uploader的GitHub readme.md中的Options並沒有特別說明有關是否可以多選的設定,

    https://github.com/valums/file-uploader

    但是在「fileuploader.js」的原始碼中是有「multiple」的這一個option,而且預設為「true」的狀態,

    image

    所以我們就可以去更改「multiple」的值來決定是否能在選取上傳檔案視窗中選取多個檔案,

    以下為使用預設為可多選的設定,

    <script src="@Url.Content("~/Scripts/fileuploader.js")" type="text/javascript"></script>
    <script type="text/javascript" language="javascript">
    <!--
        $(document).ready(function ()
        {
            createUploader();
        });
     
        function createUploader()
        {
            var uploader = new qq.FileUploader(
            {
                element: $('#file-uploader')[0],
                action: '@Url.Action("BasicUpload", "Home")'
                //multiple: true
            });
        }
    -->
    </script>

    在Firefox瀏覽器中的操作情境:

    可以選取多個檔案來上傳。

    image

    完成上傳多個檔案

    image

    而這個設定在IE瀏覽器中是完全沒作用的,至於為什麼不行,這一點我就沒有去研究了。

     

    當「multiple」的值設定為false時,在Firefox瀏覽器下,就無法進行多選上傳的操作,

    <script src="@Url.Content("~/Scripts/fileuploader.js")" type="text/javascript"></script>
    <script type="text/javascript" language="javascript">
    <!--
        $(document).ready(function ()
        {
            createUploader();
        });
     
        function createUploader()
        {
            var uploader = new qq.FileUploader(
            {
                element: $('#file-uploader')[0],
                action: '@Url.Action("BasicUpload", "Home")',
                multiple: false
            });
        }
    -->
    </script>

    image

     

     


    設定是否可以拖曳檔案?!

    在原本的valums file-uploader的發佈版本中是還沒有「dragDrop」這一個option,

    但是在valums fileuploader的GitHub Issues #116中有這樣的內容:

    https://github.com/valums/file-uploader/pull/116

    而作者後續也針對原始碼做了修改:

    https://github.com/valums/file-uploader/pull/116/files

    但是這邊必須說,絕對不要直接拿上面未發佈的原始檔來用,因為還有很多的問題,就我來說,直接拿來用是會出現錯誤的,

    因為#116的修改還包含「autoUpload」的option設定,這個並不在我們這一個修改項目中,所以我們就必須自己動手來修改這一段,

    首先在原本「fileuploader.js」中的qq.FileUploaderBasic = function(o){ …… }中去增加「dragDrop」option,

    /**
    * Creates upload button, validates upload, but doesn't create file list or dd. 
    */
    qq.FileUploaderBasic = function (o)
    {
        this._options = {
            // set to true to see the server response
            debug: false,
            action: '/server/upload',
            params: {},
            button: null,
            multiple: true,
            dragDrop: true,
            maxConnections: 3,
            // validation        
            allowedExtensions: [],
            sizeLimit: 0,
            minSizeLimit: 0,
            // events
            // return false to cancel submit
            onSubmit: function (id, fileName) { },
            onProgress: function (id, fileName, loaded, total) { },
            onComplete: function (id, fileName, responseJSON) { },
            onCancel: function (id, fileName) { },
            // messages                
            messages: {
                typeError: "{file} has invalid extension. Only {extensions} are allowed.",
                sizeError: "{file} is too large, maximum file size is {sizeLimit}.",
                minSizeError: "{file} is too small, minimum file size is {minSizeLimit}.",
                emptyError: "{file} is empty, please select files again without it.",
                onLeave: "The files are being uploaded, if you leave now the upload will be cancelled."
            },
            showMessage: function (message)
            {
                alert(message);
            }
        };

    然後在原本預設template的地方去做一點修改,

    //        template: '<div class="qq-uploader">' +
    //                '<div class="qq-upload-drop-area"><span>Drop files here to upload</span></div>' +
    //                '<div class="qq-upload-button">Upload a file</div>' +
    //                '<ul class="qq-upload-list"></ul>' +
    //             '</div>',
     
            template: (typeof o.dragDrop != "undefined" && !o.dragDrop
                        ?
                            '<div class="qq-uploader">' +
                                '<div class="qq-upload-button">Upload a file</div>' +
                                '<ul class="qq-upload-list"></ul>' +
                            '</div>'
                        :
                            '<div class="qq-uploader">' +
                                '<div class="qq-upload-drop-area"><span>Drop files here to upload</span></div>' +
                                '<div class="qq-upload-button">Upload a file</div>' +
                                '<ul class="qq-upload-list"></ul>' +
                            '</div>'
                       ),

    修改後的「fileuploader.1.js」內容:

    /**
    * http://github.com/valums/file-uploader
    * 
    * Multiple file upload component with progress-bar, drag-and-drop. 
    * © 2010 Andrew Valums ( andrew(at)valums.com ) 
    * 
    * Licensed under GNU GPL 2 or later and GNU LGPL 2 or later, see license.txt.
    */
     
    //
    // Helper functions
    //
     
    var qq = qq || {};
     
    /**
    * Adds all missing properties from second obj to first obj
    */
    qq.extend = function (first, second)
    {
        for (var prop in second)
        {
            first[prop] = second[prop];
        }
    };
     
    /**
    * Searches for a given element in the array, returns -1 if it is not present.
    * @param {Number} [from] The index at which to begin the search
    */
    qq.indexOf = function (arr, elt, from)
    {
        if (arr.indexOf) return arr.indexOf(elt, from);
     
        from = from || 0;
        var len = arr.length;
     
        if (from < 0) from += len;
     
        for (; from < len; from++)
        {
            if (from in arr && arr[from] === elt)
            {
                return from;
            }
        }
        return -1;
    };
     
    qq.getUniqueId = (function ()
    {
        var id = 0;
        return function () { return id++; };
    })();
     
    //
    // Events
     
    qq.attach = function (element, type, fn)
    {
        if (element.addEventListener)
        {
            element.addEventListener(type, fn, false);
        } else if (element.attachEvent)
        {
            element.attachEvent('on' + type, fn);
        }
    };
    qq.detach = function (element, type, fn)
    {
        if (element.removeEventListener)
        {
            element.removeEventListener(type, fn, false);
        } else if (element.attachEvent)
        {
            element.detachEvent('on' + type, fn);
        }
    };
     
    qq.preventDefault = function (e)
    {
        if (e.preventDefault)
        {
            e.preventDefault();
        } else
        {
            e.returnValue = false;
        }
    };
     
    //
    // Node manipulations
     
    /**
    * Insert node a before node b.
    */
    qq.insertBefore = function (a, b)
    {
        b.parentNode.insertBefore(a, b);
    };
    qq.remove = function (element)
    {
        element.parentNode.removeChild(element);
    };
     
    qq.contains = function (parent, descendant)
    {
        // compareposition returns false in this case
        if (parent == descendant) return true;
     
        if (parent.contains)
        {
            return parent.contains(descendant);
        } else
        {
            return !!(descendant.compareDocumentPosition(parent) & 8);
        }
    };
     
    /**
    * Creates and returns element from html string
    * Uses innerHTML to create an element
    */
    qq.toElement = (function ()
    {
        var div = document.createElement('div');
        return function (html)
        {
            div.innerHTML = html;
            var element = div.firstChild;
            div.removeChild(element);
            return element;
        };
    })();
     
    //
    // Node properties and attributes
     
    /**
    * Sets styles for an element.
    * Fixes opacity in IE6-8.
    */
    qq.css = function (element, styles)
    {
        if (styles.opacity != null)
        {
            if (typeof element.style.opacity != 'string' && typeof (element.filters) != 'undefined')
            {
                styles.filter = 'alpha(opacity=' + Math.round(100 * styles.opacity) + ')';
            }
        }
        qq.extend(element.style, styles);
    };
    qq.hasClass = function (element, name)
    {
        var re = new RegExp('(^| )' + name + '( |$)');
        return re.test(element.className);
    };
    qq.addClass = function (element, name)
    {
        if (!qq.hasClass(element, name))
        {
            element.className += ' ' + name;
        }
    };
    qq.removeClass = function (element, name)
    {
        var re = new RegExp('(^| )' + name + '( |$)');
        element.className = element.className.replace(re, ' ').replace(/^\s+|\s+$/g, "");
    };
    qq.setText = function (element, text)
    {
        element.innerText = text;
        element.textContent = text;
    };
     
    //
    // Selecting elements
     
    qq.children = function (element)
    {
        var children = [],
        child = element.firstChild;
     
        while (child)
        {
            if (child.nodeType == 1)
            {
                children.push(child);
            }
            child = child.nextSibling;
        }
     
        return children;
    };
     
    qq.getByClass = function (element, className)
    {
        if (element.querySelectorAll)
        {
            return element.querySelectorAll('.' + className);
        }
     
        var result = [];
        var candidates = element.getElementsByTagName("*");
        var len = candidates.length;
     
        for (var i = 0; i < len; i++)
        {
            if (qq.hasClass(candidates[i], className))
            {
                result.push(candidates[i]);
            }
        }
        return result;
    };
     
    /**
    * obj2url() takes a json-object as argument and generates
    * a querystring. pretty much like jQuery.param()
    * 
    * how to use:
    *
    *    `qq.obj2url({a:'b',c:'d'},'http://any.url/upload?otherParam=value');`
    *
    * will result in:
    *
    *    `http://any.url/upload?otherParam=value&a=b&c=d`
    *
    * @param  Object JSON-Object
    * @param  String current querystring-part
    * @return String encoded querystring
    */
    qq.obj2url = function (obj, temp, prefixDone)
    {
        var uristrings = [],
            prefix = '&',
            add = function (nextObj, i)
            {
                var nextTemp = temp
                    ? (/\[\]$/.test(temp)) // prevent double-encoding
                       ? temp
                       : temp + '[' + i + ']'
                    : i;
                if ((nextTemp != 'undefined') && (i != 'undefined'))
                {
                    uristrings.push(
                        (typeof nextObj === 'object')
                            ? qq.obj2url(nextObj, nextTemp, true)
                            : (Object.prototype.toString.call(nextObj) === '[object Function]')
                                ? encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj())
                                : encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj)
                    );
                }
            };
     
        if (!prefixDone && temp)
        {
            prefix = (/\?/.test(temp)) ? (/\?$/.test(temp)) ? '' : '&' : '?';
            uristrings.push(temp);
            uristrings.push(qq.obj2url(obj));
        } else if ((Object.prototype.toString.call(obj) === '[object Array]') && (typeof obj != 'undefined'))
        {
            // we wont use a for-in-loop on an array (performance)
            for (var i = 0, len = obj.length; i < len; ++i)
            {
                add(obj[i], i);
            }
        } else if ((typeof obj != 'undefined') && (obj !== null) && (typeof obj === "object"))
        {
            // for anything else but a scalar, we will use for-in-loop
            for (var i in obj)
            {
                add(obj[i], i);
            }
        } else
        {
            uristrings.push(encodeURIComponent(temp) + '=' + encodeURIComponent(obj));
        }
     
        return uristrings.join(prefix)
                         .replace(/^&/, '')
                         .replace(/%20/g, '+');
    };
     
    //
    //
    // Uploader Classes
    //
    //
     
    var qq = qq || {};
     
    /**
    * Creates upload button, validates upload, but doesn't create file list or dd. 
    */
    qq.FileUploaderBasic = function (o)
    {
        this._options = {
            // set to true to see the server response
            debug: false,
            action: '/server/upload',
            params: {},
            button: null,
            multiple: true,
            dragDrop: true,
            maxConnections: 3,
            // validation        
            allowedExtensions: [],
            sizeLimit: 0,
            minSizeLimit: 0,
            // events
            // return false to cancel submit
            onSubmit: function (id, fileName) { },
            onProgress: function (id, fileName, loaded, total) { },
            onComplete: function (id, fileName, responseJSON) { },
            onCancel: function (id, fileName) { },
            // messages                
            messages: {
                typeError: "{file} has invalid extension. Only {extensions} are allowed.",
                sizeError: "{file} is too large, maximum file size is {sizeLimit}.",
                minSizeError: "{file} is too small, minimum file size is {minSizeLimit}.",
                emptyError: "{file} is empty, please select files again without it.",
                onLeave: "The files are being uploaded, if you leave now the upload will be cancelled."
            },
            showMessage: function (message)
            {
                alert(message);
            }
        };
        qq.extend(this._options, o);
     
        // number of files being uploaded
        this._filesInProgress = 0;
        this._handler = this._createUploadHandler();
     
        if (this._options.button)
        {
            this._button = this._createUploadButton(this._options.button);
        }
     
        this._preventLeaveInProgress();
    };
     
    qq.FileUploaderBasic.prototype = {
        setParams: function (params)
        {
            this._options.params = params;
        },
        getInProgress: function ()
        {
            return this._filesInProgress;
        },
        _createUploadButton: function (element)
        {
            var self = this;
     
            return new qq.UploadButton({
                element: element,
                multiple: this._options.multiple && qq.UploadHandlerXhr.isSupported(),
                onChange: function (input)
                {
                    self._onInputChange(input);
                }
            });
        },
        _createUploadHandler: function ()
        {
            var self = this,
                handlerClass;
     
            if (qq.UploadHandlerXhr.isSupported())
            {
                handlerClass = 'UploadHandlerXhr';
            } else
            {
                handlerClass = 'UploadHandlerForm';
            }
     
            var handler = new qq[handlerClass]({
                debug: this._options.debug,
                action: this._options.action,
                maxConnections: this._options.maxConnections,
                onProgress: function (id, fileName, loaded, total)
                {
                    self._onProgress(id, fileName, loaded, total);
                    self._options.onProgress(id, fileName, loaded, total);
                },
                onComplete: function (id, fileName, result)
                {
                    self._onComplete(id, fileName, result);
                    self._options.onComplete(id, fileName, result);
                },
                onCancel: function (id, fileName)
                {
                    self._onCancel(id, fileName);
                    self._options.onCancel(id, fileName);
                }
            });
     
            return handler;
        },
        _preventLeaveInProgress: function ()
        {
            var self = this;
     
            qq.attach(window, 'beforeunload', function (e)
            {
                if (!self._filesInProgress) { return; }
     
                var e = e || window.event;
                // for ie, ff
                e.returnValue = self._options.messages.onLeave;
                // for webkit
                return self._options.messages.onLeave;
            });
        },
        _onSubmit: function (id, fileName)
        {
            this._filesInProgress++;
        },
        _onProgress: function (id, fileName, loaded, total)
        {
        },
        _onComplete: function (id, fileName, result)
        {
            this._filesInProgress--;
            if (result.error)
            {
                this._options.showMessage(result.error);
            }
        },
        _onCancel: function (id, fileName)
        {
            this._filesInProgress--;
        },
        _onInputChange: function (input)
        {
            if (this._handler instanceof qq.UploadHandlerXhr)
            {
                this._uploadFileList(input.files);
            } else
            {
                if (this._validateFile(input))
                {
                    this._uploadFile(input);
                }
            }
            this._button.reset();
        },
        _uploadFileList: function (files)
        {
            for (var i = 0; i < files.length; i++)
            {
                if (!this._validateFile(files[i]))
                {
                    return;
                }
            }
     
            for (var i = 0; i < files.length; i++)
            {
                this._uploadFile(files[i]);
            }
        },
        _uploadFile: function (fileContainer)
        {
            var id = this._handler.add(fileContainer);
            var fileName = this._handler.getName(id);
     
            if (this._options.onSubmit(id, fileName) !== false)
            {
                this._onSubmit(id, fileName);
                this._handler.upload(id, this._options.params);
            }
        },
        _validateFile: function (file)
        {
            var name, size;
     
            if (file.value)
            {
                // it is a file input            
                // get input value and remove path to normalize
                name = file.value.replace(/.*(\/|\\)/, "");
            } else
            {
                // fix missing properties in Safari
                name = file.fileName != null ? file.fileName : file.name;
                size = file.fileSize != null ? file.fileSize : file.size;
            }
     
            if (!this._isAllowedExtension(name))
            {
                this._error('typeError', name);
                return false;
     
            } else if (size === 0)
            {
                this._error('emptyError', name);
                return false;
     
            } else if (size && this._options.sizeLimit && size > this._options.sizeLimit)
            {
                this._error('sizeError', name);
                return false;
     
            } else if (size && size < this._options.minSizeLimit)
            {
                this._error('minSizeError', name);
                return false;
            }
     
            return true;
        },
        _error: function (code, fileName)
        {
            var message = this._options.messages[code];
            function r(name, replacement) { message = message.replace(name, replacement); }
     
            r('{file}', this._formatFileName(fileName));
            r('{extensions}', this._options.allowedExtensions.join(', '));
            r('{sizeLimit}', this._formatSize(this._options.sizeLimit));
            r('{minSizeLimit}', this._formatSize(this._options.minSizeLimit));
     
            this._options.showMessage(message);
        },
        _formatFileName: function (name)
        {
            if (name.length > 33)
            {
                name = name.slice(0, 19) + '...' + name.slice(-13);
            }
            return name;
        },
        _isAllowedExtension: function (fileName)
        {
            var ext = (-1 !== fileName.indexOf('.')) ? fileName.replace(/.*[.]/, '').toLowerCase() : '';
            var allowed = this._options.allowedExtensions;
     
            if (!allowed.length) { return true; }
     
            for (var i = 0; i < allowed.length; i++)
            {
                if (allowed[i].toLowerCase() == ext) { return true; }
            }
     
            return false;
        },
        _formatSize: function (bytes)
        {
            var i = -1;
            do
            {
                bytes = bytes / 1024;
                i++;
            } while (bytes > 99);
     
            return Math.max(bytes, 0.1).toFixed(1) + ['kB', 'MB', 'GB', 'TB', 'PB', 'EB'][i];
        }
    };
     
     
    /**
    * Class that creates upload widget with drag-and-drop and file list
    * @inherits qq.FileUploaderBasic
    */
    qq.FileUploader = function (o)
    {
        // call parent constructor
        qq.FileUploaderBasic.apply(this, arguments);
     
        // additional options    
        qq.extend(this._options, {
            element: null,
            // if set, will be used instead of qq-upload-list in template
            listElement: null,
     
    //        template: '<div class="qq-uploader">' +
    //                '<div class="qq-upload-drop-area"><span>Drop files here to upload</span></div>' +
    //                '<div class="qq-upload-button">Upload a file</div>' +
    //                '<ul class="qq-upload-list"></ul>' +
    //             '</div>',
     
            template: (typeof o.dragDrop != "undefined" && !o.dragDrop
                        ?
                            '<div class="qq-uploader">' +
                                '<div class="qq-upload-button">Upload a file</div>' +
                                '<ul class="qq-upload-list"></ul>' +
                            '</div>'
                        :
                            '<div class="qq-uploader">' +
                                '<div class="qq-upload-drop-area"><span>Drop files here to upload</span></div>' +
                                '<div class="qq-upload-button">Upload a file</div>' +
                                '<ul class="qq-upload-list"></ul>' +
                            '</div>'
                       ),
     
            // template for one item in file list
            fileTemplate: '<li>' +
                    '<span class="qq-upload-file"></span>' +
                    '<span class="qq-upload-spinner"></span>' +
                    '<span class="qq-upload-size"></span>' +
                    '<a class="qq-upload-cancel" href="#">Cancel</a>' +
                    '<span class="qq-upload-failed-text">Failed</span>' +
                '</li>',
     
            classes: {
                // used to get elements from templates
                button: 'qq-upload-button',
                drop: 'qq-upload-drop-area',
                dropActive: 'qq-upload-drop-area-active',
                list: 'qq-upload-list',
     
                file: 'qq-upload-file',
                spinner: 'qq-upload-spinner',
                size: 'qq-upload-size',
                cancel: 'qq-upload-cancel',
     
                // added to list item when upload completes
                // used in css to hide progress spinner
                success: 'qq-upload-success',
                fail: 'qq-upload-fail'
            }
        });
        // overwrite options with user supplied    
        qq.extend(this._options, o);
     
        this._element = this._options.element;
        this._element.innerHTML = this._options.template;
        this._listElement = this._options.listElement || this._find(this._element, 'list');
     
        this._classes = this._options.classes;
     
        this._button = this._createUploadButton(this._find(this._element, 'button'));
     
        this._bindCancelEvent();
        this._setupDragDrop();
    };
     
    // inherit from Basic Uploader
    qq.extend(qq.FileUploader.prototype, qq.FileUploaderBasic.prototype);
     
    qq.extend(qq.FileUploader.prototype, {
        /**
        * Gets one of the elements listed in this._options.classes
        **/
        _find: function (parent, type)
        {
            var element = qq.getByClass(parent, this._options.classes[type])[0];
            if (!element)
            {
                throw new Error('element not found ' + type);
            }
     
            return element;
        },
        _setupDragDrop: function ()
        {
            var self = this,
                dropArea = this._find(this._element, 'drop');
     
            var dz = new qq.UploadDropZone({
                element: dropArea,
                onEnter: function (e)
                {
                    qq.addClass(dropArea, self._classes.dropActive);
                    e.stopPropagation();
                },
                onLeave: function (e)
                {
                    e.stopPropagation();
                },
                onLeaveNotDescendants: function (e)
                {
                    qq.removeClass(dropArea, self._classes.dropActive);
                },
                onDrop: function (e)
                {
                    dropArea.style.display = 'none';
                    qq.removeClass(dropArea, self._classes.dropActive);
                    self._uploadFileList(e.dataTransfer.files);
                }
            });
     
            dropArea.style.display = 'none';
     
            qq.attach(document, 'dragenter', function (e)
            {
                if (!dz._isValidFileDrag(e)) return;
     
                dropArea.style.display = 'block';
            });
            qq.attach(document, 'dragleave', function (e)
            {
                if (!dz._isValidFileDrag(e)) return;
     
                var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);
                // only fire when leaving document out
                if (!relatedTarget || relatedTarget.nodeName == "HTML")
                {
                    dropArea.style.display = 'none';
                }
            });
        },
        _onSubmit: function (id, fileName)
        {
            qq.FileUploaderBasic.prototype._onSubmit.apply(this, arguments);
            this._addToList(id, fileName);
        },
        _onProgress: function (id, fileName, loaded, total)
        {
            qq.FileUploaderBasic.prototype._onProgress.apply(this, arguments);
     
            var item = this._getItemByFileId(id);
            var size = this._find(item, 'size');
            size.style.display = 'inline';
     
            var text;
            if (loaded != total)
            {
                text = Math.round(loaded / total * 100) + '% from ' + this._formatSize(total);
            } else
            {
                text = this._formatSize(total);
            }
     
            qq.setText(size, text);
        },
        _onComplete: function (id, fileName, result)
        {
            qq.FileUploaderBasic.prototype._onComplete.apply(this, arguments);
     
            // mark completed
            var item = this._getItemByFileId(id);
            qq.remove(this._find(item, 'cancel'));
            qq.remove(this._find(item, 'spinner'));
     
            if (result.success)
            {
                qq.addClass(item, this._classes.success);
            } else
            {
                qq.addClass(item, this._classes.fail);
            }
        },
        _addToList: function (id, fileName)
        {
            var item = qq.toElement(this._options.fileTemplate);
            item.qqFileId = id;
     
            var fileElement = this._find(item, 'file');
            qq.setText(fileElement, this._formatFileName(fileName));
            this._find(item, 'size').style.display = 'none';
     
            this._listElement.appendChild(item);
        },
        _getItemByFileId: function (id)
        {
            var item = this._listElement.firstChild;
     
            // there can't be txt nodes in dynamically created list
            // and we can  use nextSibling
            while (item)
            {
                if (item.qqFileId == id) return item;
                item = item.nextSibling;
            }
        },
        /**
        * delegate click event for cancel link 
        **/
        _bindCancelEvent: function ()
        {
            var self = this,
                list = this._listElement;
     
            qq.attach(list, 'click', function (e)
            {
                e = e || window.event;
                var target = e.target || e.srcElement;
     
                if (qq.hasClass(target, self._classes.cancel))
                {
                    qq.preventDefault(e);
     
                    var item = target.parentNode;
                    self._handler.cancel(item.qqFileId);
                    qq.remove(item);
                }
            });
        }
    });
     
    qq.UploadDropZone = function (o)
    {
        this._options = {
            element: null,
            onEnter: function (e) { },
            onLeave: function (e) { },
            // is not fired when leaving element by hovering descendants   
            onLeaveNotDescendants: function (e) { },
            onDrop: function (e) { }
        };
        qq.extend(this._options, o);
     
        this._element = this._options.element;
     
        this._disableDropOutside();
        this._attachEvents();
    };
     
    qq.UploadDropZone.prototype = {
        _disableDropOutside: function (e)
        {
            // run only once for all instances
            if (!qq.UploadDropZone.dropOutsideDisabled)
            {
     
                qq.attach(document, 'dragover', function (e)
                {
                    if (e.dataTransfer)
                    {
                        e.dataTransfer.dropEffect = 'none';
                        e.preventDefault();
                    }
                });
     
                qq.UploadDropZone.dropOutsideDisabled = true;
            }
        },
        _attachEvents: function ()
        {
            var self = this;
     
            qq.attach(self._element, 'dragover', function (e)
            {
                if (!self._isValidFileDrag(e)) return;
     
                var effect = e.dataTransfer.effectAllowed;
                if (effect == 'move' || effect == 'linkMove')
                {
                    e.dataTransfer.dropEffect = 'move'; // for FF (only move allowed)    
                } else
                {
                    e.dataTransfer.dropEffect = 'copy'; // for Chrome
                }
     
                e.stopPropagation();
                e.preventDefault();
            });
     
            qq.attach(self._element, 'dragenter', function (e)
            {
                if (!self._isValidFileDrag(e)) return;
     
                self._options.onEnter(e);
            });
     
            qq.attach(self._element, 'dragleave', function (e)
            {
                if (!self._isValidFileDrag(e)) return;
     
                self._options.onLeave(e);
     
                var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);
                // do not fire when moving a mouse over a descendant
                if (qq.contains(this, relatedTarget)) return;
     
                self._options.onLeaveNotDescendants(e);
            });
     
            qq.attach(self._element, 'drop', function (e)
            {
                if (!self._isValidFileDrag(e)) return;
     
                e.preventDefault();
                self._options.onDrop(e);
            });
        },
        _isValidFileDrag: function (e)
        {
            var dt = e.dataTransfer,
            // do not check dt.types.contains in webkit, because it crashes safari 4            
                isWebkit = navigator.userAgent.indexOf("AppleWebKit") > -1;
     
            // dt.effectAllowed is none in Safari 5
            // dt.types.contains check is for firefox            
            return dt && dt.effectAllowed != 'none' &&
                (dt.files || (!isWebkit && dt.types.contains && dt.types.contains('Files')));
     
        }
    };
     
    qq.UploadButton = function (o)
    {
        this._options = {
            element: null,
            // if set to true adds multiple attribute to file input      
            multiple: false,
            // name attribute of file input
            name: 'file',
            onChange: function (input) { },
            hoverClass: 'qq-upload-button-hover',
            focusClass: 'qq-upload-button-focus'
        };
     
        qq.extend(this._options, o);
     
        this._element = this._options.element;
     
        // make button suitable container for input
        qq.css(this._element, {
            position: 'relative',
            overflow: 'hidden',
            // Make sure browse button is in the right side
            // in Internet Explorer
            direction: 'ltr'
        });
     
        this._input = this._createInput();
    };
     
    qq.UploadButton.prototype = {
        /* returns file input element */
        getInput: function ()
        {
            return this._input;
        },
        /* cleans/recreates the file input */
        reset: function ()
        {
            if (this._input.parentNode)
            {
                qq.remove(this._input);
            }
     
            qq.removeClass(this._element, this._options.focusClass);
            this._input = this._createInput();
        },
        _createInput: function ()
        {
            var input = document.createElement("input");
     
            if (this._options.multiple)
            {
                input.setAttribute("multiple", "multiple");
            }
     
            input.setAttribute("type", "file");
            input.setAttribute("name", this._options.name);
     
            qq.css(input, {
                position: 'absolute',
                // in Opera only 'browse' button
                // is clickable and it is located at
                // the right side of the input
                right: 0,
                top: 0,
                fontFamily: 'Arial',
                // 4 persons reported this, the max values that worked for them were 243, 236, 236, 118
                fontSize: '118px',
                margin: 0,
                padding: 0,
                cursor: 'pointer',
                opacity: 0
            });
     
            this._element.appendChild(input);
     
            var self = this;
            qq.attach(input, 'change', function ()
            {
                self._options.onChange(input);
            });
     
            qq.attach(input, 'mouseover', function ()
            {
                qq.addClass(self._element, self._options.hoverClass);
            });
            qq.attach(input, 'mouseout', function ()
            {
                qq.removeClass(self._element, self._options.hoverClass);
            });
            qq.attach(input, 'focus', function ()
            {
                qq.addClass(self._element, self._options.focusClass);
            });
            qq.attach(input, 'blur', function ()
            {
                qq.removeClass(self._element, self._options.focusClass);
            });
     
            // IE and Opera, unfortunately have 2 tab stops on file input
            // which is unacceptable in our case, disable keyboard access
            if (window.attachEvent)
            {
                // it is IE or Opera
                input.setAttribute('tabIndex', "-1");
            }
     
            return input;
        }
    };
     
    /**
    * Class for uploading files, uploading itself is handled by child classes
    */
    qq.UploadHandlerAbstract = function (o)
    {
        this._options = {
            debug: false,
            action: '/upload.php',
            // maximum number of concurrent uploads        
            maxConnections: 999,
            onProgress: function (id, fileName, loaded, total) { },
            onComplete: function (id, fileName, response) { },
            onCancel: function (id, fileName) { }
        };
        qq.extend(this._options, o);
     
        this._queue = [];
        // params for files in queue
        this._params = [];
    };
    qq.UploadHandlerAbstract.prototype = {
        log: function (str)
        {
            if (this._options.debug && window.console) console.log('[uploader] ' + str);
        },
        /**
        * Adds file or file input to the queue
        * @returns id
        **/
        add: function (file) { },
        /**
        * Sends the file identified by id and additional query params to the server
        */
        upload: function (id, params)
        {
            var len = this._queue.push(id);
     
            var copy = {};
            qq.extend(copy, params);
            this._params[id] = copy;
     
            // if too many active uploads, wait...
            if (len <= this._options.maxConnections)
            {
                this._upload(id, this._params[id]);
            }
        },
        /**
        * Cancels file upload by id
        */
        cancel: function (id)
        {
            this._cancel(id);
            this._dequeue(id);
        },
        /**
        * Cancells all uploads
        */
        cancelAll: function ()
        {
            for (var i = 0; i < this._queue.length; i++)
            {
                this._cancel(this._queue[i]);
            }
            this._queue = [];
        },
        /**
        * Returns name of the file identified by id
        */
        getName: function (id) { },
        /**
        * Returns size of the file identified by id
        */
        getSize: function (id) { },
        /**
        * Returns id of files being uploaded or
        * waiting for their turn
        */
        getQueue: function ()
        {
            return this._queue;
        },
        /**
        * Actual upload method
        */
        _upload: function (id) { },
        /**
        * Actual cancel method
        */
        _cancel: function (id) { },
        /**
        * Removes element from queue, starts upload of next
        */
        _dequeue: function (id)
        {
            var i = qq.indexOf(this._queue, id);
            this._queue.splice(i, 1);
     
            var max = this._options.maxConnections;
     
            if (this._queue.length >= max && i < max)
            {
                var nextId = this._queue[max - 1];
                this._upload(nextId, this._params[nextId]);
            }
        }
    };
     
    /**
    * Class for uploading files using form and iframe
    * @inherits qq.UploadHandlerAbstract
    */
    qq.UploadHandlerForm = function (o)
    {
        qq.UploadHandlerAbstract.apply(this, arguments);
     
        this._inputs = {};
    };
    // @inherits qq.UploadHandlerAbstract
    qq.extend(qq.UploadHandlerForm.prototype, qq.UploadHandlerAbstract.prototype);
     
    qq.extend(qq.UploadHandlerForm.prototype, {
        add: function (fileInput)
        {
            fileInput.setAttribute('name', 'qqfile');
            var id = 'qq-upload-handler-iframe' + qq.getUniqueId();
     
            this._inputs[id] = fileInput;
     
            // remove file input from DOM
            if (fileInput.parentNode)
            {
                qq.remove(fileInput);
            }
     
            return id;
        },
        getName: function (id)
        {
            // get input value and remove path to normalize
            return this._inputs[id].value.replace(/.*(\/|\\)/, "");
        },
        _cancel: function (id)
        {
            this._options.onCancel(id, this.getName(id));
     
            delete this._inputs[id];
     
            var iframe = document.getElementById(id);
            if (iframe)
            {
                // to cancel request set src to something else
                // we use src="javascript:false;" because it doesn't
                // trigger ie6 prompt on https
                iframe.setAttribute('src', 'javascript:false;');
     
                qq.remove(iframe);
            }
        },
        _upload: function (id, params)
        {
            var input = this._inputs[id];
     
            if (!input)
            {
                throw new Error('file with passed id was not added, or already uploaded or cancelled');
            }
     
            var fileName = this.getName(id);
     
            var iframe = this._createIframe(id);
            var form = this._createForm(iframe, params);
            form.appendChild(input);
     
            var self = this;
            this._attachLoadEvent(iframe, function ()
            {
                self.log('iframe loaded');
     
                var response = self._getIframeContentJSON(iframe);
     
                self._options.onComplete(id, fileName, response);
                self._dequeue(id);
     
                delete self._inputs[id];
                // timeout added to fix busy state in FF3.6
                setTimeout(function ()
                {
                    qq.remove(iframe);
                }, 1);
            });
     
            form.submit();
            qq.remove(form);
     
            return id;
        },
        _attachLoadEvent: function (iframe, callback)
        {
            qq.attach(iframe, 'load', function ()
            {
                // when we remove iframe from dom
                // the request stops, but in IE load
                // event fires
                if (!iframe.parentNode)
                {
                    return;
                }
     
                // fixing Opera 10.53
                if (iframe.contentDocument &&
                    iframe.contentDocument.body &&
                    iframe.contentDocument.body.innerHTML == "false")
                {
                    // In Opera event is fired second time
                    // when body.innerHTML changed from false
                    // to server response approx. after 1 sec
                    // when we upload file with iframe
                    return;
                }
     
                callback();
            });
        },
        /**
        * Returns json object received by iframe from server.
        */
        _getIframeContentJSON: function (iframe)
        {
            // iframe.contentWindow.document - for IE<7
            var doc = iframe.contentDocument ? iframe.contentDocument : iframe.contentWindow.document,
                response;
     
            this.log("converting iframe's innerHTML to JSON");
            this.log("innerHTML = " + doc.body.innerHTML);
     
            try
            {
                response = eval("(" + doc.body.innerHTML + ")");
            } catch (err)
            {
                response = {};
            }
     
            return response;
        },
        /**
        * Creates iframe with unique name
        */
        _createIframe: function (id)
        {
            // We can't use following code as the name attribute
            // won't be properly registered in IE6, and new window
            // on form submit will open
            // var iframe = document.createElement('iframe');
            // iframe.setAttribute('name', id);
     
            var iframe = qq.toElement('<iframe src="javascript:false;" name="' + id + '" />');
            // src="javascript:false;" removes ie6 prompt on https
     
            iframe.setAttribute('id', id);
     
            iframe.style.display = 'none';
            document.body.appendChild(iframe);
     
            return iframe;
        },
        /**
        * Creates form, that will be submitted to iframe
        */
        _createForm: function (iframe, params)
        {
            // We can't use the following code in IE6
            // var form = document.createElement('form');
            // form.setAttribute('method', 'post');
            // form.setAttribute('enctype', 'multipart/form-data');
            // Because in this case file won't be attached to request
            var form = qq.toElement('<form method="post" enctype="multipart/form-data"></form>');
     
            var queryString = qq.obj2url(params, this._options.action);
     
            form.setAttribute('action', queryString);
            form.setAttribute('target', iframe.name);
            form.style.display = 'none';
            document.body.appendChild(form);
     
            return form;
        }
    });
     
    /**
    * Class for uploading files using xhr
    * @inherits qq.UploadHandlerAbstract
    */
    qq.UploadHandlerXhr = function (o)
    {
        qq.UploadHandlerAbstract.apply(this, arguments);
     
        this._files = [];
        this._xhrs = [];
     
        // current loaded size in bytes for each file 
        this._loaded = [];
    };
     
    // static method
    qq.UploadHandlerXhr.isSupported = function ()
    {
        var input = document.createElement('input');
        input.type = 'file';
     
        return (
            'multiple' in input &&
            typeof File != "undefined" &&
            typeof (new XMLHttpRequest()).upload != "undefined");
    };
     
    // @inherits qq.UploadHandlerAbstract
    qq.extend(qq.UploadHandlerXhr.prototype, qq.UploadHandlerAbstract.prototype)
     
    qq.extend(qq.UploadHandlerXhr.prototype, {
        /**
        * Adds file to the queue
        * Returns id to use with upload, cancel
        **/
        add: function (file)
        {
            if (!(file instanceof File))
            {
                throw new Error('Passed obj in not a File (in qq.UploadHandlerXhr)');
            }
     
            return this._files.push(file) - 1;
        },
        getName: function (id)
        {
            var file = this._files[id];
            // fix missing name in Safari 4
            return file.fileName != null ? file.fileName : file.name;
        },
        getSize: function (id)
        {
            var file = this._files[id];
            return file.fileSize != null ? file.fileSize : file.size;
        },
        /**
        * Returns uploaded bytes for file identified by id 
        */
        getLoaded: function (id)
        {
            return this._loaded[id] || 0;
        },
        /**
        * Sends the file identified by id and additional query params to the server
        * @param {Object} params name-value string pairs
        */
        _upload: function (id, params)
        {
            var file = this._files[id],
                name = this.getName(id),
                size = this.getSize(id);
     
            this._loaded[id] = 0;
     
            var xhr = this._xhrs[id] = new XMLHttpRequest();
            var self = this;
     
            xhr.upload.onprogress = function (e)
            {
                if (e.lengthComputable)
                {
                    self._loaded[id] = e.loaded;
                    self._options.onProgress(id, name, e.loaded, e.total);
                }
            };
     
            xhr.onreadystatechange = function ()
            {
                if (xhr.readyState == 4)
                {
                    self._onComplete(id, xhr);
                }
            };
     
            // build query string
            params = params || {};
            params['qqfile'] = name;
            var queryString = qq.obj2url(params, this._options.action);
     
            xhr.open("POST", queryString, true);
            xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
            xhr.setRequestHeader("X-File-Name", encodeURIComponent(name));
            xhr.setRequestHeader("Content-Type", "application/octet-stream");
            xhr.send(file);
        },
        _onComplete: function (id, xhr)
        {
            // the request was aborted/cancelled
            if (!this._files[id]) return;
     
            var name = this.getName(id);
            var size = this.getSize(id);
     
            this._options.onProgress(id, name, size, size);
     
            if (xhr.status == 200)
            {
                this.log("xhr - server response received");
                this.log("responseText = " + xhr.responseText);
     
                var response;
     
                try
                {
                    response = eval("(" + xhr.responseText + ")");
                } catch (err)
                {
                    response = {};
                }
     
                this._options.onComplete(id, name, response);
     
            } else
            {
                this._options.onComplete(id, name, {});
            }
     
            this._files[id] = null;
            this._xhrs[id] = null;
            this._dequeue(id);
        },
        _cancel: function (id)
        {
            this._options.onCancel(id, this.getName(id));
     
            this._files[id] = null;
     
            if (this._xhrs[id])
            {
                this._xhrs[id].abort();
                this._xhrs[id] = null;
            }
        }
    });

    而實際使用的程式如下:

    <script src="@Url.Content("~/Scripts/fileuploader.1.js")" type="text/javascript"></script>
    <script type="text/javascript" language="javascript">
    <!--
        $(document).ready(function ()
        {
            createUploader();
        });
     
        function createUploader()
        {
            var uploader = new qq.FileUploader(
            {
                element: $('#file-uploader')[0],
                action: '@Url.Action("BasicUpload", "Home")',
                multiple: true,
                dragDrop: false
            });
        }
    -->
    </script>

    而執行程式後,將檔案拖曳到「Upload a file」的區塊上就不會顯示原本會出現的拖曳區塊,

    image

    原本拖曳檔案時會出現的拖曳區塊

    image

    BUT…..

    沒錯,出現這個「BUT」是很弔詭的,那是因為我們設定了這個「dragDrop: false」之後是不會讓檔案拖曳到頁面上時出現拖曳區塊,

    但是不出現並不代表就不能上傳呀!!!

    沒錯,拖曳區塊不會出現也無法讓拖曳並且上傳的功能給關閉,所以「dragDrop: false」這個option的設定就只能是去設定是否顯示拖曳區塊,而無法關閉拖曳檔案上傳的功能

    如果各位有人有試出來可以把拖曳檔案上傳的功能給關閉的方式,就請分享一下修改的方法,謝謝。

     



    修改拖曳時可以上傳一個檔案而不可上傳多個檔案

    好吧…我承認上一項的進階操作修改是很OOXX的,不過接下來這個就會比較實用了,

    在本篇的第一項進階操作修改「設定是否可以多選檔案上傳」,我們可以針對開啟的選擇檔案視窗去限制是否可以選擇多個檔案或是只能選一個檔案,

    但是這個功能一旦遇到了拖曳……嘿嘿!就給他完完全全的破功了,

     

    單純的點選「Upload a file」只能在上傳檔案的視窗中去選一個檔案上傳:

    image

     

    但是拖曳一堆的檔案到頁面時卻還是可以將所有檔案給上傳:

    image

    image

     

    為了避免這樣的情況發生,我所做的修改方式是對原本的「fileuploader.js」做了一些修改,

    這邊就不去對修改的地方逐一的說明,用幾張比較的擷圖讓大家可以清楚那些地方做了修改,

    左邊是修改過的版本,右邊的原始版本:

    image

    image

    image

    image

    image

    修改後的程式完整內容,可以將程式完整內容複製並且另存為「fileuploader.mrkt.js

    /**
    * http://github.com/valums/file-uploader
    * 
    * Multiple file upload component with progress-bar, drag-and-drop. 
    * © 2010 Andrew Valums ( andrew(at)valums.com ) 
    * 
    * Licensed under GNU GPL 2 or later and GNU LGPL 2 or later, see license.txt.
    */
     
    //
    // Helper functions
    //
     
    //
    // 2012-02-13 Modified by mrkt
    // http://kevintsengtw.blogspot.com
    //
     
     
    var qq = qq || {};
     
    /**
    * Adds all missing properties from second obj to first obj
    */
    qq.extend = function (first, second)
    {
        for (var prop in second)
        {
            first[prop] = second[prop];
        }
    };
     
    /**
    * Searches for a given element in the array, returns -1 if it is not present.
    * @param {Number} [from] The index at which to begin the search
    */
    qq.indexOf = function (arr, elt, from)
    {
        if (arr.indexOf) return arr.indexOf(elt, from);
     
        from = from || 0;
        var len = arr.length;
     
        if (from < 0) from += len;
     
        for (; from < len; from++)
        {
            if (from in arr && arr[from] === elt)
            {
                return from;
            }
        }
        return -1;
    };
     
    qq.getUniqueId = (function ()
    {
        var id = 0;
        return function () { return id++; };
    })();
     
    //
    // Events
     
    qq.attach = function (element, type, fn)
    {
        if (element.addEventListener)
        {
            element.addEventListener(type, fn, false);
        } else if (element.attachEvent)
        {
            element.attachEvent('on' + type, fn);
        }
    };
    qq.detach = function (element, type, fn)
    {
        if (element.removeEventListener)
        {
            element.removeEventListener(type, fn, false);
        } else if (element.attachEvent)
        {
            element.detachEvent('on' + type, fn);
        }
    };
     
    qq.preventDefault = function (e)
    {
        if (e.preventDefault)
        {
            e.preventDefault();
        } else
        {
            e.returnValue = false;
        }
    };
     
    //
    // Node manipulations
     
    /**
    * Insert node a before node b.
    */
    qq.insertBefore = function (a, b)
    {
        b.parentNode.insertBefore(a, b);
    };
    qq.remove = function (element)
    {
        element.parentNode.removeChild(element);
    };
     
    qq.contains = function (parent, descendant)
    {
        // compareposition returns false in this case
        if (parent == descendant) return true;
     
        if (parent.contains)
        {
            return parent.contains(descendant);
        } else
        {
            return !!(descendant.compareDocumentPosition(parent) & 8);
        }
    };
     
    /**
    * Creates and returns element from html string
    * Uses innerHTML to create an element
    */
    qq.toElement = (function ()
    {
        var div = document.createElement('div');
        return function (html)
        {
            div.innerHTML = html;
            var element = div.firstChild;
            div.removeChild(element);
            return element;
        };
    })();
     
    //
    // Node properties and attributes
     
    /**
    * Sets styles for an element.
    * Fixes opacity in IE6-8.
    */
    qq.css = function (element, styles)
    {
        if (styles.opacity != null)
        {
            if (typeof element.style.opacity != 'string' && typeof (element.filters) != 'undefined')
            {
                styles.filter = 'alpha(opacity=' + Math.round(100 * styles.opacity) + ')';
            }
        }
        qq.extend(element.style, styles);
    };
    qq.hasClass = function (element, name)
    {
        var re = new RegExp('(^| )' + name + '( |$)');
        return re.test(element.className);
    };
    qq.addClass = function (element, name)
    {
        if (!qq.hasClass(element, name))
        {
            element.className += ' ' + name;
        }
    };
    qq.removeClass = function (element, name)
    {
        var re = new RegExp('(^| )' + name + '( |$)');
        element.className = element.className.replace(re, ' ').replace(/^\s+|\s+$/g, "");
    };
    qq.setText = function (element, text)
    {
        element.innerText = text;
        element.textContent = text;
    };
     
    //
    // Selecting elements
     
    qq.children = function (element)
    {
        var children = [],
        child = element.firstChild;
     
        while (child)
        {
            if (child.nodeType == 1)
            {
                children.push(child);
            }
            child = child.nextSibling;
        }
     
        return children;
    };
     
    qq.getByClass = function (element, className)
    {
        if (element.querySelectorAll)
        {
            return element.querySelectorAll('.' + className);
        }
     
        var result = [];
        var candidates = element.getElementsByTagName("*");
        var len = candidates.length;
     
        for (var i = 0; i < len; i++)
        {
            if (qq.hasClass(candidates[i], className))
            {
                result.push(candidates[i]);
            }
        }
        return result;
    };
     
    /**
    * obj2url() takes a json-object as argument and generates
    * a querystring. pretty much like jQuery.param()
    * 
    * how to use:
    *
    *    `qq.obj2url({a:'b',c:'d'},'http://any.url/upload?otherParam=value');`
    *
    * will result in:
    *
    *    `http://any.url/upload?otherParam=value&a=b&c=d`
    *
    * @param  Object JSON-Object
    * @param  String current querystring-part
    * @return String encoded querystring
    */
    qq.obj2url = function (obj, temp, prefixDone)
    {
        var uristrings = [],
            prefix = '&',
            add = function (nextObj, i)
            {
                var nextTemp = temp
                    ? (/\[\]$/.test(temp)) // prevent double-encoding
                       ? temp
                       : temp + '[' + i + ']'
                    : i;
                if ((nextTemp != 'undefined') && (i != 'undefined'))
                {
                    uristrings.push(
                        (typeof nextObj === 'object')
                            ? qq.obj2url(nextObj, nextTemp, true)
                            : (Object.prototype.toString.call(nextObj) === '[object Function]')
                                ? encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj())
                                : encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj)
                    );
                }
            };
     
        if (!prefixDone && temp)
        {
            prefix = (/\?/.test(temp)) ? (/\?$/.test(temp)) ? '' : '&' : '?';
            uristrings.push(temp);
            uristrings.push(qq.obj2url(obj));
        } else if ((Object.prototype.toString.call(obj) === '[object Array]') && (typeof obj != 'undefined'))
        {
            // we wont use a for-in-loop on an array (performance)
            for (var i = 0, len = obj.length; i < len; ++i)
            {
                add(obj[i], i);
            }
        } else if ((typeof obj != 'undefined') && (obj !== null) && (typeof obj === "object"))
        {
            // for anything else but a scalar, we will use for-in-loop
            for (var i in obj)
            {
                add(obj[i], i);
            }
        } else
        {
            uristrings.push(encodeURIComponent(temp) + '=' + encodeURIComponent(obj));
        }
     
        return uristrings.join(prefix)
                         .replace(/^&/, '')
                         .replace(/%20/g, '+');
    };
     
    //
    //
    // Uploader Classes
    //
    //
     
    var qq = qq || {};
     
    /**
    * Creates upload button, validates upload, but doesn't create file list or dd. 
    */
    qq.FileUploaderBasic = function (o)
    {
        this._options = {
            // set to true to see the server response
            debug: false,
            action: '/server/upload',
            params: {},
            button: null,
            multiple: true,
            dragDrop: true,
            maxConnections: 3,
            // validation        
            allowedExtensions: [],
            sizeLimit: 0,
            minSizeLimit: 0,
            // events
            // return false to cancel submit
            onSubmit: function (id, fileName) { },
            onProgress: function (id, fileName, loaded, total) { },
            onComplete: function (id, fileName, responseJSON) { },
            onCancel: function (id, fileName) { },
            // messages                
            messages: {
                typeError: "{file} has invalid extension. Only {extensions} are allowed.",
                sizeError: "{file} is too large, maximum file size is {sizeLimit}.",
                minSizeError: "{file} is too small, minimum file size is {minSizeLimit}.",
                emptyError: "{file} is empty, please select files again without it.",
                onLeave: "The files are being uploaded, if you leave now the upload will be cancelled."
            },
            showMessage: function (message)
            {
                alert(message);
            }
        };
        qq.extend(this._options, o);
     
        // number of files being uploaded
        this._filesInProgress = 0;
        this._handler = this._createUploadHandler();
     
        if (this._options.button)
        {
            this._button = this._createUploadButton(this._options.button);
        }
     
        this._preventLeaveInProgress();
    };
     
    qq.FileUploaderBasic.prototype = {
        setParams: function (params)
        {
            this._options.params = params;
        },
        getInProgress: function ()
        {
            return this._filesInProgress;
        },
        _createUploadButton: function (element)
        {
            var self = this;
     
            return new qq.UploadButton({
                element: element,
                multiple: this._options.multiple && qq.UploadHandlerXhr.isSupported(),
                onChange: function (input)
                {
                    self._onInputChange(input);
                }
            });
        },
        _createUploadHandler: function ()
        {
            var self = this,
                handlerClass;
     
            if (qq.UploadHandlerXhr.isSupported())
            {
                handlerClass = 'UploadHandlerXhr';
            } else
            {
                handlerClass = 'UploadHandlerForm';
            }
     
            var handler = new qq[handlerClass]({
                debug: this._options.debug,
                action: this._options.action,
                multiple: this._options.multiple,
                maxConnections: this._options.maxConnections,
                onProgress: function (id, fileName, loaded, total)
                {
                    self._onProgress(id, fileName, loaded, total);
                    self._options.onProgress(id, fileName, loaded, total);
                },
                onComplete: function (id, fileName, result)
                {
                    self._onComplete(id, fileName, result);
                    self._options.onComplete(id, fileName, result);
                },
                onCancel: function (id, fileName)
                {
                    self._onCancel(id, fileName);
                    self._options.onCancel(id, fileName);
                }
            });
     
            return handler;
        },
        _preventLeaveInProgress: function ()
        {
            var self = this;
     
            qq.attach(window, 'beforeunload', function (e)
            {
                if (!self._filesInProgress) { return; }
     
                var e = e || window.event;
                // for ie, ff
                e.returnValue = self._options.messages.onLeave;
                // for webkit
                return self._options.messages.onLeave;
            });
        },
        _onSubmit: function (id, fileName)
        {
            this._filesInProgress++;
        },
        _onProgress: function (id, fileName, loaded, total)
        {
        },
        _onComplete: function (id, fileName, result)
        {
            this._filesInProgress--;
            if (result.error)
            {
                this._options.showMessage(result.error);
            }
        },
        _onCancel: function (id, fileName)
        {
            this._filesInProgress--;
        },
        _onInputChange: function (input)
        {
            if (this._handler instanceof qq.UploadHandlerXhr)
            {
                this._uploadFileList(input.files);
            } else
            {
                if (this._validateFile(input))
                {
                    this._uploadFile(input);
                }
            }
            this._button.reset();
        },
        _uploadFileList: function (files)
        {
            for (var i = 0; i < files.length; i++)
            {
                if (!this._validateFile(files[i]))
                {
                    return;
                }
            }
     
            if (!this._options.multiple)
            {
                this._uploadFile(files[0]);
            }
            else
            {
                for (var i = 0; i < files.length; i++)
                {
                    this._uploadFile(files[i]);
                }        
            }
        },
        _uploadFile: function (fileContainer)
        {
            var id = this._handler.add(fileContainer);
            var fileName = this._handler.getName(id);
     
            if (this._options.onSubmit(id, fileName) !== false)
            {
                this._onSubmit(id, fileName);
                this._handler.upload(id, this._options.params);
            }
        },
        _validateFile: function (file)
        {
            var name, size;
     
            if (file.value)
            {
                // it is a file input            
                // get input value and remove path to normalize
                name = file.value.replace(/.*(\/|\\)/, "");
            } else
            {
                // fix missing properties in Safari
                name = file.fileName != null ? file.fileName : file.name;
                size = file.fileSize != null ? file.fileSize : file.size;
            }
     
            if (!this._isAllowedExtension(name))
            {
                this._error('typeError', name);
                return false;
     
            } else if (size === 0)
            {
                this._error('emptyError', name);
                return false;
     
            } else if (size && this._options.sizeLimit && size > this._options.sizeLimit)
            {
                this._error('sizeError', name);
                return false;
     
            } else if (size && size < this._options.minSizeLimit)
            {
                this._error('minSizeError', name);
                return false;
            }
     
            return true;
        },
        _error: function (code, fileName)
        {
            var message = this._options.messages[code];
            function r(name, replacement) { message = message.replace(name, replacement); }
     
            r('{file}', this._formatFileName(fileName));
            r('{extensions}', this._options.allowedExtensions.join(', '));
            r('{sizeLimit}', this._formatSize(this._options.sizeLimit));
            r('{minSizeLimit}', this._formatSize(this._options.minSizeLimit));
     
            this._options.showMessage(message);
        },
        _formatFileName: function (name)
        {
            if (name.length > 33)
            {
                name = name.slice(0, 19) + '...' + name.slice(-13);
            }
            return name;
        },
        _isAllowedExtension: function (fileName)
        {
            var ext = (-1 !== fileName.indexOf('.')) ? fileName.replace(/.*[.]/, '').toLowerCase() : '';
            var allowed = this._options.allowedExtensions;
     
            if (!allowed.length) { return true; }
     
            for (var i = 0; i < allowed.length; i++)
            {
                if (allowed[i].toLowerCase() == ext) { return true; }
            }
     
            return false;
        },
        _formatSize: function (bytes)
        {
            var i = -1;
            do
            {
                bytes = bytes / 1024;
                i++;
            } while (bytes > 99);
     
            return Math.max(bytes, 0.1).toFixed(1) + ['kB', 'MB', 'GB', 'TB', 'PB', 'EB'][i];
        }
    };
     
     
    /**
    * Class that creates upload widget with drag-and-drop and file list
    * @inherits qq.FileUploaderBasic
    */
    qq.FileUploader = function (o)
    {
        // call parent constructor
        qq.FileUploaderBasic.apply(this, arguments);
     
        // additional options    
        qq.extend(this._options, {
            element: null,
            // if set, will be used instead of qq-upload-list in template
            listElement: null,
     
    //        template: '<div class="qq-uploader">' +
    //                '<div class="qq-upload-drop-area"><span>Drop files here to upload</span></div>' +
    //                '<div class="qq-upload-button">Upload a file</div>' +
    //                '<ul class="qq-upload-list"></ul>' +
    //             '</div>',
     
            template: (typeof o.dragDrop != "undefined" && !o.dragDrop 
                        ? 
                            '<div class="qq-uploader">' +    
                                '<div class="qq-upload-button">Upload a file</div>' +    
                                '<ul class="qq-upload-list"></ul>' +    
                            '</div>' 
                        : 
                            '<div class="qq-uploader">' +    
                                '<div class="qq-upload-drop-area"><span>Drop files here to upload</span></div>' +    
                                '<div class="qq-upload-button">Upload a file</div>' +    
                                '<ul class="qq-upload-list"></ul>' +
                            '</div>'
                       ),
     
            // template for one item in file list
            fileTemplate: '<li>' +
                    '<span class="qq-upload-file"></span>' +
                    '<span class="qq-upload-spinner"></span>' +
                    '<span class="qq-upload-size"></span>' +
                    '<a class="qq-upload-cancel" href="#">Cancel</a>' +
                    '<span class="qq-upload-failed-text">Failed</span>' +
                '</li>',
     
            classes: {
                // used to get elements from templates
                button: 'qq-upload-button',
                drop: 'qq-upload-drop-area',
                dropActive: 'qq-upload-drop-area-active',
                list: 'qq-upload-list',
     
                file: 'qq-upload-file',
                spinner: 'qq-upload-spinner',
                size: 'qq-upload-size',
                cancel: 'qq-upload-cancel',
     
                // added to list item when upload completes
                // used in css to hide progress spinner
                success: 'qq-upload-success',
                fail: 'qq-upload-fail'
            }
        });
        // overwrite options with user supplied    
        qq.extend(this._options, o);
     
        this._element = this._options.element;
        this._element.innerHTML = this._options.template;
        this._listElement = this._options.listElement || this._find(this._element, 'list');
     
        this._classes = this._options.classes;
     
        this._button = this._createUploadButton(this._find(this._element, 'button'));
     
        this._bindCancelEvent();
        if (this._options.dragDrop) this._setupDragDrop();
    };
     
    // inherit from Basic Uploader
    qq.extend(qq.FileUploader.prototype, qq.FileUploaderBasic.prototype);
     
    qq.extend(qq.FileUploader.prototype, {
        /**
        * Gets one of the elements listed in this._options.classes
        **/
        _find: function (parent, type)
        {
            var element = qq.getByClass(parent, this._options.classes[type])[0];
            if (!element)
            {
                throw new Error('element not found ' + type);
            }
     
            return element;
        },
        _setupDragDrop: function ()
        {
            var self = this,
                dropArea = this._find(this._element, 'drop');
     
            var dz = new qq.UploadDropZone({
                element: dropArea,
                onEnter: function (e)
                {
                    qq.addClass(dropArea, self._classes.dropActive);
                    e.stopPropagation();
                },
                onLeave: function (e)
                {
                    e.stopPropagation();
                },
                onLeaveNotDescendants: function (e)
                {
                    qq.removeClass(dropArea, self._classes.dropActive);
                },
                onDrop: function (e)
                {
                    dropArea.style.display = 'none';
                    qq.removeClass(dropArea, self._classes.dropActive);
                    self._uploadFileList(e.dataTransfer.files);
                }
            });
     
            dropArea.style.display = 'none';
     
            qq.attach(document, 'dragenter', function (e)
            {
                if (!dz._isValidFileDrag(e)) return;