PageSourceSearch

https://badmintonzone.pl/lib/fineuploader/jquery.fineuploader-3.0.js

js badmintonzone.pl collected 2026-10-02 20:39:09 UTC 70,794 bytes, 2,124 lines download raw bytes

1/**
2 * http://github.com/Valums-File-Uploader/file-uploader
3 *
4 * Multiple file upload component with progress-bar, drag-and-drop, support for all modern browsers.
5 *
6 * Original version: 1.0 © 2010 Andrew Valums ( andrew(at)valums.com )
7 * Current Maintainer (2.0+): © 2012, Ray Nicholus ( fineuploader(at)garstasio.com )
8 *
9 * Licensed under MIT license, GNU GPL 2 or later, GNU LGPL 2 or later, see license.txt.
10 */
11
12var qq = qq || {};
13var qq = function(element) {
14    "use strict";
15
16    return {
17        hide: function() {
18            element.style.display = 'none';
19            return this;
20        },
21
22        /** Returns the function which detaches attached event */
23        attach: function(type, fn) {
24            if (element.addEventListener){
25                element.addEventListener(type, fn, false);
26            } else if (element.attachEvent){
27                element.attachEvent('on' + type, fn);
28            }
29            return function() {
30                qq(element).detach(type, fn);
31            };
32        },
33
34        detach: function(type, fn) {
35            if (element.removeEventListener){
36                element.removeEventListener(type, fn, false);
37            } else if (element.attachEvent){
38                element.detachEvent('on' + type, fn);
39            }
40            return this;
41        },
42
43        contains: function(descendant) {
44            // compareposition returns false in this case
45            if (element == descendant) {
46                return true;
47            }
48
49            if (element.contains){
50                return element.contains(descendant);
51            } else {
52                return !!(descendant.compareDocumentPosition(element) & 8);
53            }
54        },
55
56        /**
57         * Insert this element before elementB.
58         */
59        insertBefore: function(elementB) {
60            elementB.parentNode.insertBefore(element, elementB);
61            return this;
62        },
63
64        remove: function() {
65            element.parentNode.removeChild(element);
66            return this;
67        },
68
69        /**
70         * Sets styles for an element.
71         * Fixes opacity in IE6-8.
72         */
73        css: function(styles) {
74            if (styles.opacity != null){
75                if (typeof element.style.opacity != 'string' && typeof(element.filters) != 'undefined'){
76                    styles.filter = 'alpha(opacity=' + Math.round(100 * styles.opacity) + ')';
77                }
78            }
79            qq.extend(element.style, styles);
80
81            return this;
82        },
83
84        hasClass: function(name) {
85            var re = new RegExp('(^| )' + name + '( |$)');
86            return re.test(element.className);
87        },
88
89        addClass: function(name) {
90            if (!qq(element).hasClass(name)){
91                element.className += ' ' + name;
92            }
93            return this;
94        },
95
96        removeClass: function(name) {
97            var re = new RegExp('(^| )' + name + '( |$)');
98            element.className = element.className.replace(re, ' ').replace(/^\s+|\s+$/g, "");
99            return this;
100        },
101
102        getByClass: function(className) {
103            if (element.querySelectorAll){
104                return element.querySelectorAll('.' + className);
105            }
106
107            var result = [];
108            var candidates = element.getElementsByTagName("*");
109            var len = candidates.length;
110
111            for (var i = 0; i < len; i++){
112                if (qq(candidates[i]).hasClass(className)){
113                    result.push(candidates[i]);
114                }
115            }
116            return result;
117        },
118
119        children: function() {
120            var children = [],
121                child = element.firstChild;
122
123            while (child){
124                if (child.nodeType == 1){
125                    children.push(child);
126                }
127                child = child.nextSibling;
128            }
129
130            return children;
131        },
132
133        setText: function(text) {
134            element.innerText = text;
135            element.textContent = text;
136            return this;
137        },
138
139        clearText: function() {
140            return qq(element).setText("");
141        }
142    };
143};
144
145qq.log = function(message, level) {
146    if (window.console) {
147        if (!level || level === 'info') {
148            window.console.log(message);
149        }
150        else
151        {
152            if (window.console[level]) {
153                window.console[level](message);
154            }
155            else {
156                window.console.log('<' + level + '> ' + message);
157            }
158        }
159    }
160};
161
162qq.isObject = function(variable) {
163    "use strict";
164    return variable !== null && variable && typeof(variable) === "object" && variable.constructor === Object;
165};
166
167qq.extend = function (first, second, extendNested) {
168    "use strict";
169    var prop;
170    for (prop in second) {
171        if (second.hasOwnProperty(prop)) {
172            if (extendNested && qq.isObject(second[prop])) {
173                if (first[prop] === undefined) {
174                    first[prop] = {};
175                }
176                qq.extend(first[prop], second[prop], true);
177            }
178            else {
179                first[prop] = second[prop];
180            }
181        }
182    }
183};
184
185/**
186 * Searches for a given element in the array, returns -1 if it is not present.
187 * @param {Number} [from] The index at which to begin the search
188 */
189qq.indexOf = function(arr, elt, from){
190    if (arr.indexOf) return arr.indexOf(elt, from);
191
192    from = from || 0;
193    var len = arr.length;
194
195    if (from < 0) from += len;
196
197    for (; from < len; from++){
198        if (from in arr && arr[from] === elt){
199            return from;
200        }
201    }
202    return -1;
203};
204
205qq.getUniqueId = (function(){
206    var id = 0;
207    return function(){ return id++; };
208})();
209
210//
211// Browsers and platforms detection
212
213qq.ie       = function(){ return navigator.userAgent.indexOf('MSIE') != -1; }
214qq.ie10     = function(){ return navigator.userAgent.indexOf('MSIE 10') != -1; }
215qq.safari   = function(){ return navigator.vendor != undefined && navigator.vendor.indexOf("Apple") != -1; }
216qq.chrome   = function(){ return navigator.vendor != undefined && navigator.vendor.indexOf('Google') != -1; }
217qq.firefox  = function(){ return (navigator.userAgent.indexOf('Mozilla') != -1 && navigator.vendor != undefined && navigator.vendor == ''); }
218qq.windows  = function(){ return navigator.platform == "Win32"; }
219
220//
221// Events
222
223qq.preventDefault = function(e){
224    if (e.preventDefault){
225        e.preventDefault();
226    } else{
227        e.returnValue = false;
228    }
229};
230
231/**
232 * Creates and returns element from html string
233 * Uses innerHTML to create an element
234 */
235qq.toElement = (function(){
236    var div = document.createElement('div');
237    return function(html){
238        div.innerHTML = html;
239        var element = div.firstChild;
240        div.removeChild(element);
241        return element;
242    };
243})();
244
245/**
246 * obj2url() takes a json-object as argument and generates
247 * a querystring. pretty much like jQuery.param()
248 *
249 * how to use:
250 *
251 *    `qq.obj2url({a:'b',c:'d'},'http://any.url/upload?otherParam=value');`
252 *
253 * will result in:
254 *
255 *    `http://any.url/upload?otherParam=value&a=b&c=d`
256 *
257 * @param  Object JSON-Object
258 * @param  String current querystring-part
259 * @return String encoded querystring
260 */
261qq.obj2url = function(obj, temp, prefixDone){
262    var uristrings = [],
263        prefix = '&',
264        add = function(nextObj, i){
265            var nextTemp = temp
266                ? (/\[\]$/.test(temp)) // prevent double-encoding
267                ? temp
268                : temp+'['+i+']'
269                : i;
270            if ((nextTemp != 'undefined') && (i != 'undefined')) {
271                uristrings.push(
272                    (typeof nextObj === 'object')
273                        ? qq.obj2url(nextObj, nextTemp, true)
274                        : (Object.prototype.toString.call(nextObj) === '[object Function]')
275                        ? encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj())
276                        : encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj)
277                );
278            }
279        };
280
281    if (!prefixDone && temp) {
282        prefix = (/\?/.test(temp)) ? (/\?$/.test(temp)) ? '' : '&' : '?';
283        uristrings.push(temp);
284        uristrings.push(qq.obj2url(obj));
285    } else if ((Object.prototype.toString.call(obj) === '[object Array]') && (typeof obj != 'undefined') ) {
286        // we wont use a for-in-loop on an array (performance)
287        for (var i = 0, len = obj.length; i < len; ++i){
288            add(obj[i], i);
289        }
290    } else if ((typeof obj != 'undefined') && (obj !== null) && (typeof obj === "object")){
291        // for anything else but a scalar, we will use for-in-loop
292        for (var i in obj){
293            add(obj[i], i);
294        }
295    } else {
296        uristrings.push(encodeURIComponent(temp) + '=' + encodeURIComponent(obj));
297    }
298
299    if (temp) {
300        return uristrings.join(prefix);
301    } else {
302        return uristrings.join(prefix)
303            .replace(/^&/, '')
304            .replace(/%20/g, '+');
305    }
306};
307
308/**
309 * A generic module which supports object disposing in dispose() method.
310 * */
311qq.DisposeSupport = {
312    _disposers: [],
313
314    /** Run all registered disposers */
315    dispose: function() {
316        var disposer;
317        while (disposer = this._disposers.shift()) {
318            disposer();
319        }
320    },
321
322    /** Add disposer to the collection */
323    addDisposer: function(disposeFunction) {
324        this._disposers.push(disposeFunction);
325    },
326
327    /** Attach event handler and register de-attacher as a disposer */
328    _attach: function() {
329        this.addDisposer(qq(arguments[0]).attach.apply(this, Array.prototype.slice.call(arguments, 1)));
330    }
331};
332qq.UploadButton = function(o){
333    this._options = {
334        element: null,
335        // if set to true adds multiple attribute to file input
336        multiple: false,
337        acceptFiles: null,
338        // name attribute of file input
339        name: 'file',
340        onChange: function(input){},
341        hoverClass: 'qq-upload-button-hover',
342        focusClass: 'qq-upload-button-focus'
343    };
344
345    qq.extend(this._options, o);
346    qq.extend(this, qq.DisposeSupport);
347
348    this._element = this._options.element;
349
350    // make button suitable container for input
351    qq(this._element).css({
352        position: 'relative',
353        overflow: 'hidden',
354        // Make sure browse button is in the right side
355        // in Internet Explorer
356        direction: 'ltr'
357    });
358
359    this._input = this._createInput();
360};
361
362qq.UploadButton.prototype = {
363    /* returns file input element */
364    getInput: function(){
365        return this._input;
366    },
367    /* cleans/recreates the file input */
368    reset: function(){
369        if (this._input.parentNode){
370            qq(this._input).remove();
371        }
372
373        qq(this._element).removeClass(this._options.focusClass);
374        this._input = this._createInput();
375    },
376    _createInput: function(){
377        var input = document.createElement("input");
378
379        if (this._options.multiple){
380            input.setAttribute("multiple", "multiple");
381        }
382
383        if (this._options.acceptFiles) input.setAttribute("accept", this._options.acceptFiles);
384
385        input.setAttribute("type", "file");
386        input.setAttribute("name", this._options.name);
387
388        qq(input).css({
389            position: 'absolute',
390            // in Opera only 'browse' button
391            // is clickable and it is located at
392            // the right side of the input
393            right: 0,
394            top: 0,
395            fontFamily: 'Arial',
396            // 4 persons reported this, the max values that worked for them were 243, 236, 236, 118
397            fontSize: '118px',
398            margin: 0,
399            padding: 0,
400            cursor: 'pointer',
401            opacity: 0
402        });
403
404        this._element.appendChild(input);
405
406        var self = this;
407        this._attach(input, 'change', function(){
408            self._options.onChange(input);
409        });
410
411        this._attach(input, 'mouseover', function(){
412            qq(self._element).addClass(self._options.hoverClass);
413        });
414        this._attach(input, 'mouseout', function(){
415            qq(self._element).removeClass(self._options.hoverClass);
416        });
417        this._attach(input, 'focus', function(){
418            qq(self._element).addClass(self._options.focusClass);
419        });
420        this._attach(input, 'blur', function(){
421            qq(self._element).removeClass(self._options.focusClass);
422        });
423
424        // IE and Opera, unfortunately have 2 tab stops on file input
425        // which is unacceptable in our case, disable keyboard access
426        if (window.attachEvent){
427            // it is IE or Opera
428            input.setAttribute('tabIndex', "-1");
429        }
430
431        return input;
432    }
433};
434qq.FineUploaderBasic = function(o){
435    var that = this;
436    this._options = {
437        debug: false,
438        button: null,
439        multiple: true,
440        maxConnections: 3,
441        disableCancelForFormUploads: false,
442        autoUpload: true,
443        request: {
444            endpoint: '/server/upload',
445            params: {},
446            customHeaders: {},
447            forceMultipart: false,
448            inputName: 'qqfile'
449        },
450        validation: {
451            allowedExtensions: [],
452            sizeLimit: 0,
453            minSizeLimit: 0,
454            stopOnFirstInvalidFile: true
455        },
456        callbacks: {
457            onSubmit: function(id, fileName){}, // return false to cancel submit
458            onComplete: function(id, fileName, responseJSON){},
459            onCancel: function(id, fileName){},
460            onUpload: function(id, fileName, xhr){},
461            onProgress: function(id, fileName, loaded, total){},
462            onError: function(id, fileName, reason) {},
463            onAutoRetry: function(id, fileName, attemptNumber) {},
464            onManualRetry: function(id, fileName) {},
465            onValidate: function(fileData) {} // return false to prevent upload
466        },
467        messages: {
468            typeError: "{file} has an invalid extension. Valid extension(s): {extensions}.",
469            sizeError: "{file} is too large, maximum file size is {sizeLimit}.",
470            minSizeError: "{file} is too small, minimum file size is {minSizeLimit}.",
471            emptyError: "{file} is empty, please select files again without it.",
472            noFilesError: "No files to upload.",
473            onLeave: "The files are being uploaded, if you leave now the upload will be cancelled."
474        },
475        retry: {
476            enableAuto: false,
477            maxAutoAttempts: 3,
478            autoAttemptDelay: 5,
479            preventRetryResponseProperty: 'preventRetry'
480        }
481    };
482
483    qq.extend(this._options, o, true);
484    this._wrapCallbacks();
485    qq.extend(this, qq.DisposeSupport);
486
487    // number of files being uploaded
488    this._filesInProgress = 0;
489
490    this._storedFileIds = [];
491
492    this._autoRetries = [];
493    this._retryTimeouts = [];
494    this._preventRetries = [];
495
496    this._handler = this._createUploadHandler();
497
498    if (this._options.button){
499        this._button = this._createUploadButton(this._options.button);
500    }
501
502    this._preventLeaveInProgress();
503};
504
505qq.FineUploaderBasic.prototype = {
506    log: function(str, level) {
507        if (this._options.debug && (!level || level === 'info')) {
508            qq.log('[FineUploader] ' + str);
509        }
510        else if (level && level !== 'info') {
511            qq.log('[FineUploader] ' + str, level);
512
513        }
514    },
515    setParams: function(params){
516        this._options.request.params = params;
517    },
518    getInProgress: function(){
519        return this._filesInProgress;
520    },
521    uploadStoredFiles: function(){
522        "use strict";
523        while(this._storedFileIds.length) {
524            this._filesInProgress++;
525            this._handler.upload(this._storedFileIds.shift(), this._options.request.params);
526        }
527    },
528    clearStoredFiles: function(){
529        this._storedFileIds = [];
530    },
531    retry: function(id) {
532        if (this._onBeforeManualRetry(id)) {
533            this._handler.retry(id);
534            return true;
535        }
536        else {
537            return false;
538        }
539    },
540    cancel: function(fileId) {
541        this._handler.cancel(fileId);
542    },
543    reset: function() {
544        this.log("Resetting uploader...");
545        this._handler.reset();
546        this._filesInProgress = 0;
547        this._storedFileIds = [];
548        this._autoRetries = [];
549        this._retryTimeouts = [];
550        this._preventRetries = [];
551        this._button.reset();
552    },
553    _createUploadButton: function(element){
554        var self = this;
555
556        var button = new qq.UploadButton({
557            element: element,
558            multiple: this._options.multiple && qq.UploadHandlerXhr.isSupported(),
559            acceptFiles: this._options.validation.acceptFiles,
560            onChange: function(input){
561                self._onInputChange(input);
562            }
563        });
564
565        this.addDisposer(function() { button.dispose(); });
566        return button;
567    },
568    _createUploadHandler: function(){
569        var self = this,
570            handlerClass;
571
572        if(qq.UploadHandlerXhr.isSupported()){
573            handlerClass = 'UploadHandlerXhr';
574        } else {
575            handlerClass = 'UploadHandlerForm';
576        }
577
578        var handler = new qq[handlerClass]({
579            debug: this._options.debug,
580            endpoint: this._options.request.endpoint,
581            forceMultipart: this._options.request.forceMultipart,
582            maxConnections: this._options.maxConnections,
583            customHeaders: this._options.request.customHeaders,
584            inputName: this._options.request.inputName,
585            demoMode: this._options.demoMode,
586            log: this.log,
587            onProgress: function(id, fileName, loaded, total){
588                self._onProgress(id, fileName, loaded, total);
589                self._options.callbacks.onProgress(id, fileName, loaded, total);
590            },
591            onComplete: function(id, fileName, result, xhr){
592                self._onComplete(id, fileName, result, xhr);
593                self._options.callbacks.onComplete(id, fileName, result);
594            },
595            onCancel: function(id, fileName){
596                self._onCancel(id, fileName);
597                self._options.callbacks.onCancel(id, fileName);
598            },
599            onUpload: function(id, fileName, xhr){
600                self._onUpload(id, fileName, xhr);
601                self._options.callbacks.onUpload(id, fileName, xhr);
602            },
603            onAutoRetry: function(id, fileName, responseJSON, xhr) {
604                self._preventRetries[id] = responseJSON[self._options.retry.preventRetryResponseProperty];
605
606                if (self._shouldAutoRetry(id, fileName, responseJSON)) {
607                    self._maybeParseAndSendUploadError(id, fileName, responseJSON, xhr);
608                    self._options.callbacks.onAutoRetry(id, fileName, self._autoRetries[id] + 1);
609                    self._onBeforeAutoRetry(id, fileName);
610
611                    self._retryTimeouts[id] = setTimeout(function() {
612                        self._onAutoRetry(id, fileName, responseJSON)
613                    }, self._options.retry.autoAttemptDelay * 1000);
614
615                    return true;
616                }
617                else {
618                    return false;
619                }
620            }
621        });
622
623        return handler;
624    },
625    _preventLeaveInProgress: function(){
626        var self = this;
627
628        this._attach(window, 'beforeunload', function(e){
629            if (!self._filesInProgress){return;}
630
631            var e = e || window.event;
632            // for ie, ff
633            e.returnValue = self._options.messages.onLeave;
634            // for webkit
635            return self._options.messages.onLeave;
636        });
637    },
638    _onSubmit: function(id, fileName){
639        if (this._options.autoUpload) {
640            this._filesInProgress++;
641        }
642    },
643    _onProgress: function(id, fileName, loaded, total){
644    },
645    _onComplete: function(id, fileName, result, xhr){
646        this._filesInProgress--;
647        this._maybeParseAndSendUploadError(id, fileName, result, xhr);
648    },
649    _onCancel: function(id, fileName){
650        clearTimeout(this._retryTimeouts[id]);
651
652        var storedFileIndex = qq.indexOf(this._storedFileIds, id);
653        if (this._options.autoUpload || storedFileIndex < 0) {
654            this._filesInProgress--;
655        }
656        else if (!this._options.autoUpload) {
657            this._storedFileIds.splice(storedFileIndex, 1);
658        }
659    },
660    _onUpload: function(id, fileName, xhr){
661    },
662    _onInputChange: function(input){
663        if (this._handler instanceof qq.UploadHandlerXhr){
664            this._uploadFileList(input.files);
665        } else {
666            if (this._validateFile(input)){
667                this._uploadFile(input);
668            }
669        }
670        this._button.reset();
671    },
672    _onBeforeAutoRetry: function(id, fileName) {
673        this.log("Waiting " + this._options.retry.autoAttemptDelay + " seconds before retrying " + fileName + "...");
674    },
675    _onAutoRetry: function(id, fileName, responseJSON) {
676        this.log("Retrying " + fileName + "...");
677        this._autoRetries[id]++;
678        this._handler.retry(id);
679    },
680    _shouldAutoRetry: function(id, fileName, responseJSON) {
681        if (!this._preventRetries[id] && this._options.retry.enableAuto) {
682            if (this._autoRetries[id] === undefined) {
683                this._autoRetries[id] = 0;
684            }
685
686            return this._autoRetries[id] < this._options.retry.maxAutoAttempts
687        }
688
689        return false;
690    },
691    //return false if we should not attempt the requested retry
692    _onBeforeManualRetry: function(id) {
693        if (this._preventRetries[id]) {
694            this.log("Retries are forbidden for id " + id, 'warn');
695            return false;
696        }
697        else if (this._handler.isValid(id)) {
698            var fileName = this._handler.getName(id);
699
700            if (this._options.callbacks.onManualRetry(id, fileName) === false) {
701                return false;
702            }
703
704            this.log("Retrying upload for '" + fileName + "' (id: " + id + ")...");
705            this._filesInProgress++;
706            return true;
707        }
708        else {
709            this.log("'" + id + "' is not a valid file ID", 'error');
710            return false;
711        }
712    },
713    _maybeParseAndSendUploadError: function(id, fileName, response, xhr) {
714        //assuming no one will actually set the response code to something other than 200 and still set 'success' to true
715        if (!response.success){
716            if (xhr && xhr.status !== 200 && !response.error) {
717                this._options.callbacks.onError(id, fileName, "XHR returned response code " + xhr.status);
718            }
719            else {
720                var errorReason = response.error ? response.error : "Upload failure reason unknown";
721                this._options.callbacks.onError(id, fileName, errorReason);
722            }
723        }
724    },
725    _uploadFileList: function(files){
726        var validationDescriptors, index, batchInvalid;
727
728        validationDescriptors = this._getValidationDescriptors(files);
729        if (validationDescriptors.length > 1) {
730            batchInvalid = this._options.callbacks.onValidate(validationDescriptors) === false;
731        }
732
733        if (!batchInvalid) {
734            if (files.length > 0) {
735                for (index = 0; index < files.length; index++){
736                    if (this._validateFile(files[index])){
737                        this._uploadFile(files[index]);
738                    } else {
739                        if (this._options.validation.stopOnFirstInvalidFile){
740                            return;
741                        }
742                    }
743                }
744            }
745            else {
746                this._error('noFilesError', "");
747            }
748        }
749    },
750    _uploadFile: function(fileContainer){
751        var id = this._handler.add(fileContainer);
752        var fileName = this._handler.getName(id);
753
754        if (this._options.callbacks.onSubmit(id, fileName) !== false){
755            this._onSubmit(id, fileName);
756            if (this._options.autoUpload) {
757                this._handler.upload(id, this._options.request.params);
758            }
759            else {
760                this._storeFileForLater(id);
761            }
762        }
763    },
764    _storeFileForLater: function(id) {
765        this._storedFileIds.push(id);
766    },
767    _validateFile: function(file){
768        var validationDescriptor, name, size;
769
770        validationDescriptor = this._getValidationDescriptor(file);
771        name = validationDescriptor.name;
772        size = validationDescriptor.size;
773
774        if (this._options.callbacks.onValidate([validationDescriptor]) === false) {
775            return false;
776        }
777
778        if (!this._isAllowedExtension(name)){
779            this._error('typeError', name);
780            return false;
781
782        }
783        else if (size === 0){
784            this._error('emptyError', name);
785            return false;
786
787        }
788        else if (size && this._options.validation.sizeLimit && size > this._options.validation.sizeLimit){
789            this._error('sizeError', name);
790            return false;
791
792        }
793        else if (size && size < this._options.validation.minSizeLimit){
794            this._error('minSizeError', name);
795            return false;
796        }
797
798        return true;
799    },
800    _error: function(code, fileName){
801        var message = this._options.messages[code];
802        function r(name, replacement){ message = message.replace(name, replacement); }
803
804        var extensions = this._options.validation.allowedExtensions.join(', ');
805
806        r('{file}', this._formatFileName(fileName));
807        r('{extensions}', extensions);
808        r('{sizeLimit}', this._formatSize(this._options.validation.sizeLimit));
809        r('{minSizeLimit}', this._formatSize(this._options.validation.minSizeLimit));
810
811        this._options.callbacks.onError(null, fileName, message);
812
813        return message;
814    },
815    _formatFileName: function(name){
816        if (name.length > 33){
817            name = name.slice(0, 19) + '...' + name.slice(-13);
818        }
819        return name;
820    },
821    _isAllowedExtension: function(fileName){
822        var ext = (-1 !== fileName.indexOf('.'))
823            ? fileName.replace(/.*[.]/, '').toLowerCase()
824            : '';
825        var allowed = this._options.validation.allowedExtensions;
826
827        if (!allowed.length){return true;}
828
829        for (var i=0; i<allowed.length; i++){
830            if (allowed[i].toLowerCase() == ext){ return true;}
831        }
832
833        return false;
834    },
835    _formatSize: function(bytes){
836        var i = -1;
837        do {
838            bytes = bytes / 1024;
839            i++;
840        } while (bytes > 99);
841
842        return Math.max(bytes, 0.1).toFixed(1) + ['kB', 'MB', 'GB', 'TB', 'PB', 'EB'][i];
843    },
844    _wrapCallbacks: function() {
845        var self, safeCallback;
846
847        self = this;
848
849        safeCallback = function(name, callback, args) {
850            try {
851                return callback.apply(self, args);
852            }
853            catch (exception) {
854                self.log("Caught exception in '" + name + "' callback - " + exception, 'error');
855            }
856        }
857
858        for (var prop in this._options.callbacks) {
859            (function() {
860                var oldCallback = self._options.callbacks[prop];
861                self._options.callbacks[prop] = function() {
862                    return safeCallback(prop, oldCallback, arguments);
863                }
864            }());
865        }
866    },
867    _parseFileName: function(file) {
868        var name;
869
870        if (file.value){
871            // it is a file input
872            // get input value and remove path to normalize
873            name = file.value.replace(/.*(\/|\\)/, "");
874        } else {
875            // fix missing properties in Safari 4 and firefox 11.0a2
876            name = (file.fileName !== null && file.fileName !== undefined) ? file.fileName : file.name;
877        }
878
879        return name;
880    },
881    _parseFileSize: function(file) {
882        var size;
883
884        if (!file.value){
885            // fix missing properties in Safari 4 and firefox 11.0a2
886            size = (file.fileSize !== null && file.fileSize !== undefined) ? file.fileSize : file.size;
887        }
888
889        return size;
890    },
891    _getValidationDescriptor: function(file) {
892        var name, size, fileDescriptor;
893
894        fileDescriptor = {};
895        name = this._parseFileName(file);
896        size = this._parseFileSize(file);
897
898        fileDescriptor.name = name;
899        if (size) {
900            fileDescriptor.size = size;
901        }
902
903        return fileDescriptor;
904    },
905    _getValidationDescriptors: function(files) {
906        var index, fileDescriptors;
907
908        fileDescriptors = [];
909
910        for (index = 0; index < files.length; index++) {
911            fileDescriptors.push(files[index]);
912        }
913
914        return fileDescriptors;
915    }
916};
917/**
918 * Class that creates upload widget with drag-and-drop and file list
919 * @inherits qq.FineUploaderBasic
920 */
921qq.FineUploader = function(o){
922    // call parent constructor
923    qq.FineUploaderBasic.apply(this, arguments);
924
925    // additional options
926    qq.extend(this._options, {
927        element: null,
928        listElement: null,
929        dragAndDrop: {
930            extraDropzones: [],
931            hideDropzones: true,
932            disableDefaultDropzone: false
933        },
934        text: {
935            uploadButton: 'Upload a file',
936            cancelButton: 'Cancel',
937            retryButton: 'Retry',
938            failUpload: 'Upload failed',
939            dragZone: 'Drop files here to upload',
940            formatProgress: "{percent}% of {total_size}",
941            waitingForResponse: "Processing..."
942        },
943        template: '<div class="qq-uploader">' +
944            ((!this._options.dragAndDrop || !this._options.dragAndDrop.disableDefaultDropzone) ? '<div class="qq-upload-drop-area"><span>{dragZoneText}</span></div>' : '') +
945            (!this._options.button ? '<div class="qq-upload-button"><div>{uploadButtonText}</div></div>' : '') +
946            (!this._options.listElement ? '<ul class="qq-upload-list"></ul>' : '') +
947            '</div>',
948
949        // template for one item in file list
950        fileTemplate: '<li>' +
951            '<div class="qq-progress-bar"></div>' +
952            '<span class="qq-upload-spinner"></span>' +
953            '<span class="qq-upload-finished"></span>' +
954            '<span class="qq-upload-file"></span>' +
955            '<span class="qq-upload-size"></span>' +
956            '<a class="qq-upload-cancel" href="#">{cancelButtonText}</a>' +
957            '<a class="qq-upload-retry" href="#">{retryButtonText}</a>' +
958            '<span class="qq-upload-status-text">{statusText}</span>' +
959            '</li>',
960        classes: {
961            // used to get elements from templates
962            button: 'qq-upload-button',
963            drop: 'qq-upload-drop-area',
964            dropActive: 'qq-upload-drop-area-active',
965            dropDisabled: 'qq-upload-drop-area-disabled',
966            list: 'qq-upload-list',
967            progressBar: 'qq-progress-bar',
968            file: 'qq-upload-file',
969            spinner: 'qq-upload-spinner',
970            finished: 'qq-upload-finished',
971            retrying: 'qq-upload-retrying',
972            retryable: 'qq-upload-retryable',
973            size: 'qq-upload-size',
974            cancel: 'qq-upload-cancel',
975            retry: 'qq-upload-retry',
976            statusText: 'qq-upload-status-text',
977
978            // added to list item <li> when upload completes
979            // used in css to hide progress spinner
980            success: 'qq-upload-success',
981            fail: 'qq-upload-fail',
982
983            successIcon: null,
984            failIcon: null
985        },
986        failedUploadTextDisplay: {
987            mode: 'default', //default, custom, or none
988            maxChars: 50,
989            responseProperty: 'error',
990            enableTooltip: true
991        },
992        messages: {
993            tooManyFilesError: "You may only drop one file"
994        },
995        retry: {
996            showAutoRetryNote: true,
997            autoRetryNote: "Retrying {retryNum}/{maxAuto}...",
998            showButton: false
999        },
1000        showMessage: function(message){
1001            alert(message);
1002        }
1003    }, true);
1004
1005    // overwrite options with user supplied
1006    qq.extend(this._options, o, true);
1007    this._wrapCallbacks();
1008
1009    // overwrite the upload button text if any
1010    // same for the Cancel button and Fail message text
1011    this._options.template     = this._options.template.replace(/\{dragZoneText\}/g, this._options.text.dragZone);
1012    this._options.template     = this._options.template.replace(/\{uploadButtonText\}/g, this._options.text.uploadButton);
1013    this._options.fileTemplate = this._options.fileTemplate.replace(/\{cancelButtonText\}/g, this._options.text.cancelButton);
1014    this._options.fileTemplate = this._options.fileTemplate.replace(/\{retryButtonText\}/g, this._options.text.retryButton);
1015    this._options.fileTemplate = this._options.fileTemplate.replace(/\{statusText\}/g, "");
1016
1017    this._element = this._options.element;
1018    this._element.innerHTML = this._options.template;
1019    this._listElement = this._options.listElement || this._find(this._element, 'list');
1020
1021    this._classes = this._options.classes;
1022
1023    if (!this._button) {
1024        this._button = this._createUploadButton(this._find(this._element, 'button'));
1025    }
1026
1027    this._bindCancelAndRetryEvents();
1028    this._setupDragDrop();
1029};
1030
1031// inherit from Basic Uploader
1032qq.extend(qq.FineUploader.prototype, qq.FineUploaderBasic.prototype);
1033
1034qq.extend(qq.FineUploader.prototype, {
1035    clearStoredFiles: function() {
1036        qq.FineUploaderBasic.prototype.clearStoredFiles.apply(this, arguments);
1037        this._listElement.innerHTML = "";
1038    },
1039    addExtraDropzone: function(element){
1040        this._setupExtraDropzone(element);
1041    },
1042    removeExtraDropzone: function(element){
1043        var dzs = this._options.dragAndDrop.extraDropzones;
1044        for(var i in dzs) if (dzs[i] === element) return this._options.dragAndDrop.extraDropzones.splice(i,1);
1045    },
1046    getItemByFileId: function(id){
1047        var item = this._listElement.firstChild;
1048
1049        // there can't be txt nodes in dynamically created list
1050        // and we can  use nextSibling
1051        while (item){
1052            if (item.qqFileId == id) return item;
1053            item = item.nextSibling;
1054        }
1055    },
1056    reset: function() {
1057        qq.FineUploaderBasic.prototype.reset.apply(this, arguments);
1058        this._element.innerHTML = this._options.template;
1059        this._listElement = this._options.listElement || this._find(this._element, 'list');
1060        if (!this._options.button) {
1061            this._button = this._createUploadButton(this._find(this._element, 'button'));
1062        }
1063        this._bindCancelAndRetryEvents();
1064        this._setupDragDrop();
1065    },
1066    _leaving_document_out: function(e){
1067        return ((qq.chrome() || (qq.safari() && qq.windows())) && e.clientX == 0 && e.clientY == 0) // null coords for Chrome and Safari Windows
1068            || (qq.firefox() && !e.relatedTarget); // null e.relatedTarget for Firefox
1069    },
1070    _storeFileForLater: function(id) {
1071        qq.FineUploaderBasic.prototype._storeFileForLater.apply(this, arguments);
1072        var item = this.getItemByFileId(id);
1073        qq(this._find(item, 'spinner')).hide();
1074    },
1075    /**
1076     * Gets one of the elements listed in this._options.classes
1077     **/
1078    _find: function(parent, type){
1079        var element = qq(parent).getByClass(this._options.classes[type])[0];
1080        if (!element){
1081            throw new Error('element not found ' + type);
1082        }
1083
1084        return element;
1085    },
1086    _setupExtraDropzone: function(element){
1087        this._options.dragAndDrop.extraDropzones.push(element);
1088        this._setupDropzone(element);
1089    },
1090    _setupDropzone: function(dropArea){
1091        var self = this;
1092
1093        var dz = new qq.UploadDropZone({
1094            element: dropArea,
1095            onEnter: function(e){
1096                qq(dropArea).addClass(self._classes.dropActive);
1097                e.stopPropagation();
1098            },
1099            onLeave: function(e){
1100                //e.stopPropagation();
1101            },
1102            onLeaveNotDescendants: function(e){
1103                qq(dropArea).removeClass(self._classes.dropActive);
1104            },
1105            onDrop: function(e){
1106                if (self._options.dragAndDrop.hideDropzones) {
1107                    qq(dropArea).hide();
1108                }
1109
1110                qq(dropArea).removeClass(self._classes.dropActive);
1111                if (e.dataTransfer.files.length > 1 && !self._options.multiple) {
1112                    self._error('tooManyFilesError', "");
1113                }
1114                else {
1115                    self._uploadFileList(e.dataTransfer.files);
1116                }
1117            }
1118        });
1119
1120        this.addDisposer(function() { dz.dispose(); });
1121
1122        if (this._options.dragAndDrop.hideDropzones) {
1123            qq(dropArea).hide();
1124        }
1125    },
1126    _setupDragDrop: function(){
1127        var self, dropArea;
1128
1129        self = this;
1130
1131        if (!this._options.dragAndDrop.disableDefaultDropzone) {
1132            dropArea = this._find(this._element, 'drop');
1133            this._options.dragAndDrop.extraDropzones.push(dropArea);
1134        }
1135
1136        var dropzones = this._options.dragAndDrop.extraDropzones;
1137        var i;
1138        for (i=0; i < dropzones.length; i++){
1139            this._setupDropzone(dropzones[i]);
1140        }
1141
1142        // IE <= 9 does not support the File API used for drag+drop uploads
1143        if (!this._options.dragAndDrop.disableDefaultDropzone && (!qq.ie() || qq.ie10())) {
1144            this._attach(document, 'dragenter', function(e){
1145                if (qq(dropArea).hasClass(self._classes.dropDisabled)) return;
1146
1147                dropArea.style.display = 'block';
1148                for (i=0; i < dropzones.length; i++){ dropzones[i].style.display = 'block'; }
1149
1150            });
1151        }
1152        this._attach(document, 'dragleave', function(e){
1153            if (self._options.dragAndDrop.hideDropzones && qq.FineUploader.prototype._leaving_document_out(e)) {
1154                for (i=0; i < dropzones.length; i++) {
1155                    qq(dropzones[i]).hide();
1156                }
1157            }
1158        });
1159        qq(document).attach('drop', function(e){
1160            if (self._options.dragAndDrop.hideDropzones) {
1161                for (i=0; i < dropzones.length; i++) {
1162                    qq(dropzones[i]).hide();
1163                }
1164            }
1165            e.preventDefault();
1166        });
1167    },
1168    _onSubmit: function(id, fileName){
1169        qq.FineUploaderBasic.prototype._onSubmit.apply(this, arguments);
1170        this._addToList(id, fileName);
1171    },
1172    // Update the progress bar & percentage as the file is uploaded
1173    _onProgress: function(id, fileName, loaded, total){
1174        qq.FineUploaderBasic.prototype._onProgress.apply(this, arguments);
1175
1176        var item, progressBar, text, percent, cancelLink, size;
1177
1178        item = this.getItemByFileId(id);
1179        progressBar = this._find(item, 'progressBar');
1180        percent = Math.round(loaded / total * 100);
1181
1182        if (loaded === total) {
1183            cancelLink = this._find(item, 'cancel');
1184            qq(cancelLink).hide();
1185
1186            qq(progressBar).hide();
1187            qq(this._find(item, 'statusText')).setText(this._options.text.waitingForResponse);
1188
1189            // If last byte was sent, just display final size
1190            text = this._formatSize(total);
1191        }
1192        else {
1193            // If still uploading, display percentage
1194            text = this._formatProgress(loaded, total);
1195
1196            qq(progressBar).css({display: 'block'});
1197        }
1198
1199        // Update progress bar element
1200        qq(progressBar).css({width: percent + '%'});
1201
1202        size = this._find(item, 'size');
1203        qq(size).css({display: 'inline'});
1204        qq(size).setText(text);
1205    },
1206    _onComplete: function(id, fileName, result, xhr){
1207        qq.FineUploaderBasic.prototype._onComplete.apply(this, arguments);
1208
1209        var item = this.getItemByFileId(id);
1210
1211        qq(this._find(item, 'statusText')).clearText();
1212
1213        qq(item).removeClass(this._classes.retrying);
1214        qq(this._find(item, 'progressBar')).hide();
1215
1216        if (!this._options.disableCancelForFormUploads || qq.UploadHandlerXhr.isSupported()) {
1217            qq(this._find(item, 'cancel')).hide();
1218        }
1219        qq(this._find(item, 'spinner')).hide();
1220
1221        if (result.success){
1222            qq(item).addClass(this._classes.success);
1223            if (this._classes.successIcon) {
1224                this._find(item, 'finished').style.display = "inline-block";
1225                qq(item).addClass(this._classes.successIcon);
1226            }
1227        } else {
1228            qq(item).addClass(this._classes.fail);
1229            if (this._classes.failIcon) {
1230                this._find(item, 'finished').style.display = "inline-block";
1231                qq(item).addClass(this._classes.failIcon);
1232            }
1233            if (this._options.retry.showButton && !this._preventRetries[id]) {
1234                qq(item).addClass(this._classes.retryable);
1235            }
1236            this._controlFailureTextDisplay(item, result);
1237        }
1238    },
1239    _onUpload: function(id, fileName, xhr){
1240        qq.FineUploaderBasic.prototype._onUpload.apply(this, arguments);
1241
1242        var item = this.getItemByFileId(id);
1243        this._showSpinner(item);
1244    },
1245    _onBeforeAutoRetry: function(id) {
1246        var item, progressBar, cancelLink, failTextEl, retryNumForDisplay, maxAuto, retryNote;
1247
1248        qq.FineUploaderBasic.prototype._onBeforeAutoRetry.apply(this, arguments);
1249
1250        item = this.getItemByFileId(id);
1251        progressBar = this._find(item, 'progressBar');
1252
1253        this._showCancelLink(item);
1254        progressBar.style.width = 0;
1255        qq(progressBar).hide();
1256
1257        if (this._options.retry.showAutoRetryNote) {
1258            failTextEl = this._find(item, 'statusText');
1259            retryNumForDisplay = this._autoRetries[id] + 1;
1260            maxAuto = this._options.retry.maxAutoAttempts;
1261
1262            retryNote = this._options.retry.autoRetryNote.replace(/\{retryNum\}/g, retryNumForDisplay);
1263            retryNote = retryNote.replace(/\{maxAuto\}/g, maxAuto);
1264
1265            qq(failTextEl).setText(retryNote);
1266            if (retryNumForDisplay === 1) {
1267                qq(item).addClass(this._classes.retrying);
1268            }
1269        }
1270    },
1271    //return false if we should not attempt the requested retry
1272    _onBeforeManualRetry: function(id) {
1273        if (qq.FineUploaderBasic.prototype._onBeforeManualRetry.apply(this, arguments)) {
1274            var item = this.getItemByFileId(id);
1275            this._find(item, 'progressBar').style.width = 0;
1276            qq(item).removeClass(this._classes.fail);
1277            this._showSpinner(item);
1278            this._showCancelLink(item);
1279            return true;
1280        }
1281        return false;
1282    },
1283    _addToList: function(id, fileName){
1284        var item = qq.toElement(this._options.fileTemplate);
1285        if (this._options.disableCancelForFormUploads && !qq.UploadHandlerXhr.isSupported()) {
1286            var cancelLink = this._find(item, 'cancel');
1287            qq(cancelLink).remove();
1288        }
1289
1290        item.qqFileId = id;
1291
1292        var fileElement = this._find(item, 'file');
1293        qq(fileElement).setText(this._formatFileName(fileName));
1294        qq(this._find(item, 'size')).hide();
1295        if (!this._options.multiple) this._clearList();
1296        this._listElement.appendChild(item);
1297    },
1298    _clearList: function(){
1299        this._listElement.innerHTML = '';
1300        this.clearStoredFiles();
1301    },
1302    /**
1303     * delegate click event for cancel & retry links
1304     **/
1305    _bindCancelAndRetryEvents: function(){
1306        var self = this,
1307            list = this._listElement;
1308
1309        this._attach(list, 'click', function(e){
1310            e = e || window.event;
1311            var target = e.target || e.srcElement;
1312
1313            if (qq(target).hasClass(self._classes.cancel) || qq(target).hasClass(self._classes.retry)){
1314                qq.preventDefault(e);
1315
1316                var item = target.parentNode;
1317                while(item.qqFileId == undefined) {
1318                    item = target = target.parentNode;
1319                }
1320
1321                if (qq(target).hasClass(self._classes.cancel)) {
1322                    self.cancel(item.qqFileId);
1323                    qq(item).remove();
1324                }
1325                else {
1326                    qq(item).removeClass(self._classes.retryable);
1327                    self.retry(item.qqFileId);
1328                }
1329            }
1330        });
1331    },
1332    _formatProgress: function (uploadedSize, totalSize) {
1333        var message = this._options.text.formatProgress;
1334        function r(name, replacement) { message = message.replace(name, replacement); }
1335
1336        r('{percent}', Math.round(uploadedSize / totalSize * 100));
1337        r('{total_size}', this._formatSize(totalSize));
1338        return message;
1339    },
1340    _controlFailureTextDisplay: function(item, response) {
1341        var mode, maxChars, responseProperty, failureReason, shortFailureReason;
1342
1343        mode = this._options.failedUploadTextDisplay.mode;
1344        maxChars = this._options.failedUploadTextDisplay.maxChars;
1345        responseProperty = this._options.failedUploadTextDisplay.responseProperty;
1346
1347        if (mode === 'custom') {
1348            failureReason = response[responseProperty];
1349            if (failureReason) {
1350                if (failureReason.length > maxChars) {
1351                    shortFailureReason = failureReason.substring(0, maxChars) + '...';
1352                }
1353            }
1354            else {
1355                failureReason = this._options.text.failUpload;
1356                this.log("'" + responseProperty + "' is not a valid property on the server response.", 'w
1356arn');
1357            }
1358
1359            qq(this._find(item, 'statusText')).setText(shortFailureReason || failureReason);
1360
1361            if (this._options.failedUploadTextDisplay.enableTooltip) {
1362                this._showTooltip(item, failureReason);
1363            }
1364        }
1365        else if (mode === 'default') {
1366            qq(this._find(item, 'statusText')).setText(this._options.text.failUpload);
1367        }
1368        else if (mode !== 'none') {
1369            this.log("failedUploadTextDisplay.mode value of '" + mode + "' is not valid", 'warn');
1370        }
1371    },
1372    //TODO turn this into a real tooltip, with click trigger (so it is usable on mobile devices).  See case #355 for details.
1373    _showTooltip: function(item, text) {
1374        item.title = text;
1375    },
1376    _showSpinner: function(item) {
1377        var spinnerEl = this._find(item, 'spinner');
1378        spinnerEl.style.display = "inline-block";
1379    },
1380    _showCancelLink: function(item) {
1381        if (!this._options.disableCancelForFormUploads || qq.UploadHandlerXhr.isSupported()) {
1382            var cancelLink = this._find(item, 'cancel');
1383            cancelLink.style.display = 'inline';
1384        }
1385    },
1386    _error: function(code, fileName){
1387        var message = qq.FineUploaderBasic.prototype._error.apply(this, arguments);
1388        this._options.showMessage(message);
1389    }
1390});
1391
1392qq.UploadDropZone = function(o){
1393    this._options = {
1394        element: null,
1395        onEnter: function(e){},
1396        onLeave: function(e){},
1397        // is not fired when leaving element by hovering descendants
1398        onLeaveNotDescendants: function(e){},
1399        onDrop: function(e){}
1400    };
1401    qq.extend(this._options, o);
1402    qq.extend(this, qq.DisposeSupport);
1403
1404    this._element = this._options.element;
1405
1406    this._disableDropOutside();
1407    this._attachEvents();
1408};
1409
1410qq.UploadDropZone.prototype = {
1411    _dragover_should_be_canceled: function(){
1412        return qq.safari() || (qq.firefox() && qq.windows());
1413    },
1414    _disableDropOutside: function(e){
1415        // run only once for all instances
1416        if (!qq.UploadDropZone.dropOutsideDisabled ){
1417
1418            // for these cases we need to catch onDrop to reset dropArea
1419            if (this._dragover_should_be_canceled){
1420                qq(document).attach('dragover', function(e){
1421                    e.preventDefault();
1422                });
1423            } else {
1424                qq(document).attach('dragover', function(e){
1425                    if (e.dataTransfer){
1426                        e.dataTransfer.dropEffect = 'none';
1427                        e.preventDefault();
1428                    }
1429                });
1430            }
1431
1432            qq.UploadDropZone.dropOutsideDisabled = true;
1433        }
1434    },
1435    _attachEvents: function(){
1436        var self = this;
1437
1438        self._attach(self._element, 'dragover', function(e){
1439            if (!self._isValidFileDrag(e)) return;
1440
1441            var effect = qq.ie() ? null : e.dataTransfer.effectAllowed;
1442            if (effect == 'move' || effect == 'linkMove'){
1443                e.dataTransfer.dropEffect = 'move'; // for FF (only move allowed)
1444            } else {
1445                e.dataTransfer.dropEffect = 'copy'; // for Chrome
1446            }
1447
1448            e.stopPropagation();
1449            e.preventDefault();
1450        });
1451
1452        self._attach(self._element, 'dragenter', function(e){
1453            if (!self._isValidFileDrag(e)) return;
1454
1455            self._options.onEnter(e);
1456        });
1457
1458        self._attach(self._element, 'dragleave', function(e){
1459            if (!self._isValidFileDrag(e)) return;
1460
1461            self._options.onLeave(e);
1462
1463            var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);
1464            // do not fire when moving a mouse over a descendant
1465            if (qq(this).contains(relatedTarget)) return;
1466
1467            self._options.onLeaveNotDescendants(e);
1468        });
1469
1470        self._attach(self._element, 'drop', function(e){
1471            if (!self._isValidFileDrag(e)) return;
1472
1473            e.preventDefault();
1474            self._options.onDrop(e);
1475        });
1476    },
1477    _isValidFileDrag: function(e){
1478        // e.dataTransfer currently causing IE errors
1479        // IE9 does NOT support file API, so drag-and-drop is not possible
1480        if (qq.ie() && !qq.ie10()) return false;
1481
1482        var dt = e.dataTransfer,
1483        // do not check dt.types.contains in webkit, because it crashes safari 4
1484            isSafari = qq.safari();
1485
1486        // dt.effectAllowed is none in Safari 5
1487        // dt.types.contains check is for firefox
1488        var effectTest = qq.ie10() ? true : dt.effectAllowed != 'none';
1489        return dt && effectTest && (dt.files || (!isSafari && dt.types.contains && dt.types.contains('Files')));
1490    }
1491};
1492/**
1493 * Class for uploading files, uploading itself is handled by child classes
1494 */
1495qq.UploadHandlerAbstract = function(o){
1496    // Default options, can be overridden by the user
1497    this._options = {
1498        debug: false,
1499        endpoint: '/upload.php',
1500        // maximum number of concurrent uploads
1501        maxConnections: 999,
1502        log: function(str, level) {},
1503        onProgress: function(id, fileName, loaded, total){},
1504        onComplete: function(id, fileName, response, xhr){},
1505        onCancel: function(id, fileName){},
1506        onUpload: function(id, fileName, xhr){},
1507        onAutoRetry: function(id, fileName, response, xhr){}
1508
1509    };
1510    qq.extend(this._options, o);
1511
1512    this._queue = [];
1513    // params for files in queue
1514    this._params = [];
1515
1516    this.log = this._options.log;
1517};
1518qq.UploadHandlerAbstract.prototype = {
1519    /**
1520     * Adds file or file input to the queue
1521     * @returns id
1522     **/
1523    add: function(file){},
1524    /**
1525     * Sends the file identified by id and additional query params to the server
1526     */
1527    upload: function(id, params){
1528        var len = this._queue.push(id);
1529
1530        var copy = {};
1531        qq.extend(copy, params);
1532        this._params[id] = copy;
1533
1534        // if too many active uploads, wait...
1535        if (len <= this._options.maxConnections){
1536            this._upload(id, this._params[id]);
1537        }
1538    },
1539    retry: function(id) {
1540        var i = qq.indexOf(this._queue, id);
1541        if (i >= 0) {
1542            this._upload(id, this._params[id]);
1543        }
1544        else {
1545            this.upload(id, this._params[id]);
1546        }
1547    },
1548    /**
1549     * Cancels file upload by id
1550     */
1551    cancel: function(id){
1552        this.log('Cancelling ' + id);
1553        this._cancel(id);
1554        this._dequeue(id);
1555    },
1556    /**
1557     * Cancells all uploads
1558     */
1559    cancelAll: function(){
1560        for (var i=0; i<this._queue.length; i++){
1561            this._cancel(this._queue[i]);
1562        }
1563        this._queue = [];
1564    },
1565    /**
1566     * Returns name of the file identified by id
1567     */
1568    getName: function(id){},
1569    /**
1570     * Returns size of the file identified by id
1571     */
1572    getSize: function(id){},
1573    /**
1574     * Returns id of files being uploaded or
1575     * waiting for their turn
1576     */
1577    getQueue: function(){
1578        return this._queue;
1579    },
1580    reset: function() {
1581        this.log('Resetting upload handler');
1582        this._queue = [];
1583        this._params = [];
1584    },
1585    /**
1586     * Actual upload method
1587     */
1588    _upload: function(id){},
1589    /**
1590     * Actual cancel method
1591     */
1592    _cancel: function(id){},
1593    /**
1594     * Removes element from queue, starts upload of next
1595     */
1596    _dequeue: function(id){
1597        var i = qq.indexOf(this._queue, id);
1598        this._queue.splice(i, 1);
1599
1600        var max = this._options.maxConnections;
1601
1602        if (this._queue.length >= max && i < max){
1603            var nextId = this._queue[max-1];
1604            this._upload(nextId, this._params[nextId]);
1605        }
1606    },
1607    /**
1608     * Determine if the file exists.
1609     */
1610    isValid: function(id) {}
1611};
1612/**
1613 * Class for uploading files using form and iframe
1614 * @inherits qq.UploadHandlerAbstract
1615 */
1616qq.UploadHandlerForm = function(o){
1617    qq.UploadHandlerAbstract.apply(this, arguments);
1618
1619    this._inputs = {};
1620    this._detach_load_events = {};
1621};
1622// @inherits qq.UploadHandlerAbstract
1623qq.extend(qq.UploadHandlerForm.prototype, qq.UploadHandlerAbstract.prototype);
1624
1625qq.extend(qq.UploadHandlerForm.prototype, {
1626    add: function(fileInput){
1627        fileInput.setAttribute('name', this._options.inputName);
1628        var id = 'qq-upload-handler-iframe' + qq.getUniqueId();
1629
1630        this._inputs[id] = fileInput;
1631
1632        // remove file input from DOM
1633        if (fileInput.parentNode){
1634            qq(fileInput).remove();
1635        }
1636
1637        return id;
1638    },
1639    getName: function(id){
1640        // get input value and remove path to normalize
1641        return this._inputs[id].value.replace(/.*(\/|\\)/, "");
1642    },
1643    isValid: function(id) {
1644        return this._inputs[id] !== undefined;
1645    },
1646    reset: function() {
1647        qq.UploadHandlerAbstract.prototype.reset.apply(this, arguments);
1648        this._inputs = {};
1649        this._detach_load_events = {};
1650    },
1651    _cancel: function(id){
1652        this._options.onCancel(id, this.getName(id));
1653
1654        delete this._inputs[id];
1655        delete this._detach_load_events[id];
1656
1657        var iframe = document.getElementById(id);
1658        if (iframe){
1659            // to cancel request set src to something else
1660            // we use src="javascript:false;" because it doesn't
1661            // trigger ie6 prompt on https
1662            iframe.setAttribute('src', 'javascript:false;');
1663
1664            qq(iframe).remove();
1665        }
1666    },
1667    _upload: function(id, params){
1668        this._options.onUpload(id, this.getName(id), false);
1669        var input = this._inputs[id];
1670
1671        if (!input){
1672            throw new Error('file with passed id was not added, or already uploaded or cancelled');
1673        }
1674
1675        var fileName = this.getName(id);
1676        params[this._options.inputName] = fileName;
1677
1678        var iframe = this._createIframe(id);
1679        var form = this._createForm(iframe, params);
1680        form.appendChild(input);
1681
1682        var self = this;
1683        this._attachLoadEvent(iframe, function(){
1684            self.log('iframe loaded');
1685
1686            var response = self._getIframeContentJSON(iframe);
1687
1688            // timeout added to fix busy state in FF3.6
1689            setTimeout(function(){
1690                self._detach_load_events[id]();
1691                delete self._detach_load_events[id];
1692                qq(iframe).remove();
1693            }, 1);
1694
1695            if (!response.success) {
1696                if (self._options.onAutoRetry(id, fileName, response)) {
1697                    return;
1698                }
1699            }
1700            self._options.onComplete(id, fileName, response);
1701            self._dequeue(id);
1702        });
1703
1704        this.log('Sending upload request for ' + id);
1705        form.submit();
1706        qq(form).remove();
1707
1708        return id;
1709    },
1710    _attachLoadEvent: function(iframe, callback){
1711        var self = this;
1712        this._detach_load_events[iframe.id] = qq(iframe).attach('load', function(){
1713            self.log('Received response for ' + iframe.id);
1714
1715            // when we remove iframe from dom
1716            // the request stops, but in IE load
1717            // event fires
1718            if (!iframe.parentNode){
1719                return;
1720            }
1721
1722            try {
1723                // fixing Opera 10.53
1724                if (iframe.contentDocument &&
1725                    iframe.contentDocument.body &&
1726                    iframe.contentDocument.body.innerHTML == "false"){
1727                    // In Opera event is fired second time
1728                    // when body.innerHTML changed from false
1729                    // to server response approx. after 1 sec
1730                    // when we upload file with iframe
1731                    return;
1732                }
1733            }
1734            catch (error) {
1735                //IE may throw an "access is denied" error when attempting to access contentDocument on the iframe in some cases
1736                self.log('Error when attempting to access iframe during handling of upload response (' + error + ")", 'error');
1737            }
1738
1739            callback();
1740        });
1741    },
1742    /**
1743     * Returns json object received by iframe from server.
1744     */
1745    _getIframeContentJSON: function(iframe){
1746        //IE may throw an "access is denied" error when attempting to access contentDocument on the iframe in some cases
1747        try {
1748            // iframe.contentWindow.document - for IE<7
1749            var doc = iframe.contentDocument ? iframe.contentDocument: iframe.contentWindow.document,
1750                response;
1751
1752            var innerHTML = doc.body.innerHTML;
1753            this.log("converting iframe's innerHTML to JSON");
1754            this.log("innerHTML = " + innerHTML);
1755            //plain text response may be wrapped in <pre> tag
1756            if (innerHTML && innerHTML.match(/^<pre/i)) {
1757                innerHTML = doc.body.firstChild.firstChild.nodeValue;
1758            }
1759            response = eval("(" + innerHTML + ")");
1760        } catch(error){
1761            this.log('Error when attempting to parse form upload response (' + error + ")", 'error');
1762            response = {success: false};
1763        }
1764
1765        return response;
1766    },
1767    /**
1768     * Creates iframe with unique name
1769     */
1770    _createIframe: function(id){
1771        // We can't use following code as the name attribute
1772        // won't be properly registered in IE6, and new window
1773        // on form submit will open
1774        // var iframe = document.createElement('iframe');
1775        // iframe.setAttribute('name', id);
1776
1777        var iframe = qq.toElement('<iframe src="javascript:false;" name="' + id + '" />');
1778        // src="javascript:false;" removes ie6 prompt on https
1779
1780        iframe.setAttribute('id', id);
1781
1782        iframe.style.display = 'none';
1783        document.body.appendChild(iframe);
1784
1785        return iframe;
1786    },
1787    /**
1788     * Creates form, that will be submitted to iframe
1789     */
1790    _createForm: function(iframe, params){
1791        // We can't use the following code in IE6
1792        // var form = document.createElement('form');
1793        // form.setAttribute('method', 'post');
1794        // form.setAttribute('enctype', 'multipart/form-data');
1795        // Because in this case file won't be attached to request
1796        var protocol = this._options.demoMode ? "GET" : "POST"
1797        var form = qq.toElement('<form method="' + protocol + '" enctype="multipart/form-data"></form>');
1798
1799        var queryString = qq.obj2url(params, this._options.endpoint);
1800
1801        form.setAttribute('action', queryString);
1802        form.setAttribute('target', iframe.name);
1803        form.style.display = 'none';
1804        document.body.appendChild(form);
1805
1806        return form;
1807    }
1808});
1809/**
1810 * Class for uploading files using xhr
1811 * @inherits qq.UploadHandlerAbstract
1812 */
1813qq.UploadHandlerXhr = function(o){
1814    qq.UploadHandlerAbstract.apply(this, arguments);
1815
1816    this._files = [];
1817    this._xhrs = [];
1818
1819    // current loaded size in bytes for each file
1820    this._loaded = [];
1821};
1822
1823// static method
1824qq.UploadHandlerXhr.isSupported = function(){
1825    var input = document.createElement('input');
1826    input.type = 'file';
1827
1828    return (
1829        'multiple' in input &&
1830            typeof File != "undefined" &&
1831            typeof FormData != "undefined" &&
1832            typeof (new XMLHttpRequest()).upload != "undefined" );
1833};
1834
1835// @inherits qq.UploadHandlerAbstract
1836qq.extend(qq.UploadHandlerXhr.prototype, qq.UploadHandlerAbstract.prototype)
1837
1838qq.extend(qq.UploadHandlerXhr.prototype, {
1839    /**
1840     * Adds file to the queue
1841     * Returns id to use with upload, cancel
1842     **/
1843    add: function(file){
1844        if (!(file instanceof File)){
1845            throw new Error('Passed obj in not a File (in qq.UploadHandlerXhr)');
1846        }
1847
1848        return this._files.push(file) - 1;
1849    },
1850    getName: function(id){
1851        var file = this._files[id];
1852        // fix missing name in Safari 4
1853        //NOTE: fixed missing name firefox 11.0a2 file.fileName is actually undefined
1854        return (file.fileName !== null && file.fileName !== undefined) ? file.fileName : file.name;
1855    },
1856    getSize: function(id){
1857        var file = this._files[id];
1858        return file.fileSize != null ? file.fileSize : file.size;
1859    },
1860    /**
1861     * Returns uploaded bytes for file identified by id
1862     */
1863    getLoaded: function(id){
1864        return this._loaded[id] || 0;
1865    },
1866    isValid: function(id) {
1867        return this._files[id] !== undefined;
1868    },
1869    reset: function() {
1870        qq.UploadHandlerAbstract.prototype.reset.apply(this, arguments);
1871        this._files = [];
1872        this._xhrs = [];
1873        this._loaded = [];
1874    },
1875    /**
1876     * Sends the file identified by id and additional query params to the server
1877     * @param {Object} params name-value string pairs
1878     */
1879    _upload: function(id, params){
1880        this._options.onUpload(id, this.getName(id), true);
1881
1882        var file = this._files[id],
1883            name = this.getName(id),
1884            size = this.getSize(id);
1885
1886        this._loaded[id] = 0;
1887
1888        var xhr = this._xhrs[id] = new XMLHttpRequest();
1889        var self = this;
1890
1891        xhr.upload.onprogress = function(e){
1892            if (e.lengthComputable){
1893                self._loaded[id] = e.loaded;
1894                self._options.onProgress(id, name, e.loaded, e.total);
1895            }
1896        };
1897
1898        xhr.onreadystatechange = function(){
1899            if (xhr.readyState == 4){
1900                self._onComplete(id, xhr);
1901            }
1902        };
1903
1904        // build query string
1905        params = params || {};
1906        params[this._options.inputName] = name;
1907        var queryString = qq.obj2url(params, this._options.endpoint);
1908
1909        var protocol = this._options.demoMode ? "GET" : "POST";
1910        xhr.open(protocol, queryString, true);
1911        xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
1912        xhr.setRequestHeader("X-File-Name", encodeURIComponent(name));
1913        xhr.setRequestHeader("Cache-Control", "no-cache");
1914        if (this._options.forceMultipart) {
1915            var formData = new FormData();
1916            formData.append(this._options.inputName, file);
1917            file = formData;
1918        } else {
1919            xhr.setRequestHeader("Content-Type", "application/octet-stream");
1920            //NOTE: return mime type in xhr works on chrome 16.0.9 firefox 11.0a2
1921            xhr.setRequestHeader("X-Mime-Type",file.type );
1922        }
1923        for (key in this._options.customHeaders){
1924            xhr.setRequestHeader(key, this._options.customHeaders[key]);
1925        };
1926
1927        this.log('Sending upload request for ' + id);
1928        xhr.send(file);
1929    },
1930    _onComplete: function(id, xhr){
1931        "use strict";
1932        // the request was aborted/cancelled
1933        if (!this._files[id]) { return; }
1934
1935        var name = this.getName(id);
1936        var size = this.getSize(id);
1937        var response; //the parsed JSON response from the server, or the empty object if parsing failed.
1938
1939        this._options.onProgress(id, name, size, size);
1940
1941        this.log("xhr - server response received for " + id);
1942        this.log("responseText = " + xhr.responseText);
1943
1944        try {
1945            if (typeof JSON.parse === "function") {
1946                response = JSON.parse(xhr.responseText);
1947            } else {
1948                response = eval("(" + xhr.responseText + ")");
1949            }
1950        } catch(error){
1951            this.log('Error when attempting to parse xhr response text (' + error + ')', 'error');
1952            response = {};
1953        }
1954
1955        if (xhr.status !== 200 || !response.success){
1956            if (this._options.onAutoRetry(id, name, response, xhr)) {
1957                return;
1958            }
1959        }
1960
1961        this._options.onComplete(id, name, response, xhr);
1962
1963        this._xhrs[id] = null;
1964        this._dequeue(id);
1965    },
1966    _cancel: function(id){
1967        this._options.onCancel(id, this.getName(id));
1968
1969        this._files[id] = null;
1970
1971        if (this._xhrs[id]){
1972            this._xhrs[id].abort();
1973            this._xhrs[id] = null;
1974        }
1975    }
1976});
1977(function($) {
1978    "use strict";
1979    var uploader, $el, init, dataStore, pluginOption, pluginOptions, addCallbacks, transformOptions, isValidCommand,
1980        delegateCommand;
1981
1982    pluginOptions = ['uploaderType'];
1983
1984    init = function (options) {
1985        if (options) {
1986            var xformedOpts = transformOptions(options);
1987            addCallbacks(xformedOpts);
1988
1989            if (pluginOption('uploaderType') === 'basic') {
1990                uploader(new qq.FineUploaderBasic(xformedOpts));
1991            }
1992            else {
1993                uploader(new qq.FineUploader(xformedOpts));
1994            }
1995        }
1996
1997        return $el;
1998    };
1999
2000    dataStore = function(key, val) {
2001        var data = $el.data('fineuploader');
2002
2003        if (val) {
2004            if (data === undefined) {
2005                data = {};
2006            }
2007            data[key] = val;
2008            $el.data('fineuploader', data);
2009        }
2010        else {
2011            if (data === undefined) {
2012                return null;
2013            }
2014            return data[key];
2015        }
2016    };
2017
2018    //the underlying Fine Uploader instance is stored in jQuery's data stored, associated with the element
2019    // tied to this instance of the plug-in
2020    uploader = function(instanceToStore) {
2021        return dataStore('uploader', instanceToStore);
2022    };
2023
2024    pluginOption = function(option, optionVal) {
2025        return dataStore(option, optionVal);
2026    };
2027
2028    //implement all callbacks defined in Fine Uploader as functions that trigger appropriately names events and
2029    // return the result of executing the bound handler back to Fine Uploader
2030    addCallbacks = function(transformedOpts) {
2031        var callbacks = transformedOpts.callbacks = {};
2032
2033        $.each(new qq.FineUploaderBasic()._options.callbacks, function(prop, func) {
2034            var name, $callbackEl;
2035
2036            name = /^on(\w+)/.exec(prop)[1];
2037            name = name.substring(0, 1).toLowerCase() + name.substring(1);
2038            $callbackEl = $el;
2039
2040            callbacks[prop] = function() {
2041                var args = Array.prototype.slice.call(arguments);
2042                return $callbackEl.triggerHandler(name, args);
2043            };
2044        });
2045    };
2046
2047    //transform jQuery objects into HTMLElements, and pass along all other option properties
2048    transformOptions = function(source, dest) {
2049        var xformed, arrayVals;
2050
2051        if (dest === undefined) {
2052            if (source.uploaderType !== 'basic') {
2053                xformed = { element : $el[0] };
2054            }
2055            else {
2056                xformed = {};
2057            }
2058        }
2059        else {
2060            xformed = dest;
2061        }
2062
2063        $.each(source, function(prop, val) {
2064            if ($.inArray(prop, pluginOptions) >= 0) {
2065                pluginOption(prop, val);
2066            }
2067            else if (val instanceof $) {
2068                xformed[prop] = val[0];
2069            }
2070            else if ($.isPlainObject(val)) {
2071                xformed[prop] = {};
2072                transformOptions(val, xformed[prop]);
2073            }
2074            else if ($.isArray(val)) {
2075                arrayVals = [];
2076                $.each(val, function(idx, arrayVal) {
2077                    if (arrayVal instanceof $) {
2078                        $.merge(arrayVals, arrayVal);
2079                    }
2080                    else {
2081                        arrayVals.push(arrayVal);
2082                    }
2083                });
2084                xformed[prop] = arrayVals;
2085            }
2086            else {
2087                xformed[prop] = val;
2088            }
2089        });
2090
2091        if (dest === undefined) {
2092            return xformed;
2093        }
2094    };
2095
2096    isValidCommand = function(command) {
2097        return $.type(command) === "string" &&
2098            !command.match(/^_/) && //enforce private methods convention
2099            uploader()[command] !== undefined;
2100    };
2101
2102    //assuming we have already verified that this is a valid command, call the associated function in the underlying
2103    // Fine Uploader instance (passing along the arguments from the caller) and return the result of the call back to the caller
2104    delegateCommand = function(command) {
2105        return uploader()[command].apply(uploader(), Array.prototype.slice.call(arguments, 1));
2106    };
2107
2108    $.fn.fineUploader = function(optionsOrCommand) {
2109        $el = this;
2110
2111        if (uploader() && isValidCommand(optionsOrCommand)) {
2112            return delegateCommand.apply(this, arguments);
2113        }
2114        else if (typeof optionsOrCommand === 'object' || !optionsOrCommand) {
2115            return init.apply(this, arguments);
2116        }
2117        else {
2118            $.error('Method ' +  optionsOrCommand + ' does not exist on jQuery.fineUploader');
2119        }
2120
2121        return this;
2122    };
2123
2124}(jQuery));

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.