text: /**
   * http://github.com/Widen/fine-uploader
   *
   * Multiple file upload component with progress-bar, drag-and-drop, support for all modern browsers.
   *
   * Copyright © 2013, Widen Enterprises info@fineupoader.com
   *
   * Licensed under GNU GPL v3, see license.txt.
   */
  /*globals window, navigator, document, FormData, File, HTMLInputElement, XMLHttpRequest, Blob*/
  var qq = function(element) {
      "use strict";
  
      return {
          hide: function() {
              element.style.display = 'none';
              return this;
          },
  
          /** Returns the function which detaches attached event */
          attach: function(type, fn) {
              if (element.addEventListener){
                  element.addEventListener(type, fn, false);
              } else if (element.attachEvent){
                  element.attachEvent('on' + type, fn);
              }
              return function() {
                  qq(element).detach(type, fn);
              };
          },
  
          detach: function(type, fn) {
              if (element.removeEventListener){
                  element.removeEventListener(type, fn, false);
              } else if (element.attachEvent){
                  element.detachEvent('on' + type, fn);
              }
              return this;
          },
  
          contains: function(descendant) {
              // compareposition returns false in this case
              if (element === descendant) {
                  return true;
              }
  
              if (element.contains){
                  return element.contains(descendant);
              } else {
                  /*jslint bitwise: true*/
                  return !!(descendant.compareDocumentPosition(element) & 8);
              }
          },
  
          /**
           * Insert this element before elementB.
           */
          insertBefore: function(elementB) {
              elementB.parentNode.insertBefore(element, elementB);
              return this;
          },
  
          remove: function() {
              element.parentNode.removeChild(element);
              return this;
          },
  
          /**
           * Sets styles for an element.
           * Fixes opacity in IE6-8.
           */
          css: function(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);
  
              return this;
          },
  
          hasClass: function(name) {
              var re = new RegExp('(^| )' + name + '( |$)');
              return re.test(element.className);
          },
  
          addClass: function(name) {
              if (!qq(element).hasClass(name)){
                  element.className += ' ' + name;
              }
              return this;
          },
  
          removeClass: function(name) {
              var re = new RegExp('(^| )' + name + '( |$)');
              element.className = element.className.replace(re, ' ').replace(/^\s+|\s+$/g, "");
              return this;
          },
  
          getByClass: function(className) {
              var candidates,
                  result = [];
  
              if (element.querySelectorAll){
                  return element.querySelectorAll('.' + className);
              }
  
              candidates = element.getElementsByTagName("*");
  
              qq.each(candidates, function(idx, val) {
                  if (qq(val).hasClass(className)){
                      result.push(val);
                  }
              });
              return result;
          },
  
          children: function() {
              var children = [],
                  child = element.firstChild;
  
              while (child){
                  if (child.nodeType === 1){
                      children.push(child);
                  }
                  child = child.nextSibling;
              }
  
              return children;
          },
  
          setText: function(text) {
              element.innerText = text;
              element.textContent = text;
              return this;
          },
  
          clearText: function() {
              return qq(element).setText("");
          }
      };
  };
  
  qq.log = function(message, level) {
      "use strict";
  
      if (window.console) {
          if (!level || level === 'info') {
              window.console.log(message);
          }
          else
          {
              if (window.console[level]) {
                  window.console[level](message);
              }
              else {
                  window.console.log('<' + level + '> ' + message);
              }
          }
      }
  };
  
  qq.isObject = function(variable) {
      "use strict";
      return variable !== null && variable && typeof(variable) === "object" && variable.constructor === Object;
  };
  
  qq.isFunction = function(variable) {
      "use strict";
      return typeof(variable) === "function";
  };
  
  qq.isString = function(maybeString) {
      "use strict";
      return Object.prototype.toString.call(maybeString) === '[object String]';
  };
  
  qq.trimStr = function(string) {
      if (String.prototype.trim) {
          return string.trim();
      }
  
      return string.replace(/^\s+|\s+$/g,'');
  };
  
  qq.isFileOrInput = function(maybeFileOrInput) {
      "use strict";
      if (qq.isBlob(maybeFileOrInput) && window.File && maybeFileOrInput instanceof File) {
          return true;
      }
      else if (window.HTMLInputElement) {
          if (maybeFileOrInput instanceof HTMLInputElement) {
              if (maybeFileOrInput.type && maybeFileOrInput.type.toLowerCase() === 'file') {
                  return true;
              }
          }
      }
      else if (maybeFileOrInput.tagName) {
          if (maybeFileOrInput.tagName.toLowerCase() === 'input') {
              if (maybeFileOrInput.type && maybeFileOrInput.type.toLowerCase() === 'file') {
                  return true;
              }
          }
      }
  
      return false;
  };
  
  qq.isBlob = function(maybeBlob) {
      "use strict";
      return window.Blob && maybeBlob instanceof Blob;
  };
  
  qq.isXhrUploadSupported = function() {
      "use strict";
      var input = document.createElement('input');
      input.type = 'file';
  
      return (
          input.multiple !== undefined &&
              typeof File !== "undefined" &&
              typeof FormData !== "undefined" &&
              typeof (new XMLHttpRequest()).upload !== "undefined" );
  };
  
  qq.isFolderDropSupported = function(dataTransfer) {
      "use strict";
      return (dataTransfer.items && dataTransfer.items[0].webkitGetAsEntry);
  };
  
  qq.isFileChunkingSupported = function() {
      "use strict";
      return !qq.android() && //android's impl of Blob.slice is broken
          qq.isXhrUploadSupported() &&
          (File.prototype.slice || File.prototype.webkitSlice || File.prototype.mozSlice);
  };
  
  qq.extend = function (first, second, extendNested) {
      "use strict";
      qq.each(second, function(prop, val) {
          if (extendNested && qq.isObject(val)) {
              if (first[prop] === undefined) {
                  first[prop] = {};
              }
              qq.extend(first[prop], val, true);
          }
          else {
              first[prop] = val;
          }
      });
  };
  
  /**
   * 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){
      "use strict";
  
      if (arr.indexOf) {
          return arr.indexOf(elt, from);
      }
  
      from = from || 0;
      var len = arr.length;
  
      if (from < 0) {
          from += len;
      }
  
      for (; from < len; from+=1){
          if (arr.hasOwnProperty(from) && arr[from] === elt){
              return from;
          }
      }
      return -1;
  };
  
  //this is a version 4 UUID
  qq.getUniqueId = function(){
      "use strict";
  
      return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
          /*jslint eqeq: true, bitwise: true*/
          var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
          return v.toString(16);
      });
  };
  
  //
  // Browsers and platforms detection
  
  qq.ie       = function(){
      "use strict";
      return navigator.userAgent.indexOf('MSIE') !== -1;
  };
  qq.ie10     = function(){
      "use strict";
      return navigator.userAgent.indexOf('MSIE 10') !== -1;
  };
  qq.safari   = function(){
      "use strict";
      return navigator.vendor !== undefined && navigator.vendor.indexOf("Apple") !== -1;
  };
  qq.chrome   = function(){
      "use strict";
      return navigator.vendor !== undefined && navigator.vendor.indexOf('Google') !== -1;
  };
  qq.firefox  = function(){
      "use strict";
      return (navigator.userAgent.indexOf('Mozilla') !== -1 && navigator.vendor !== undefined && navigator.vendor === '');
  };
  qq.windows  = function(){
      "use strict";
      return navigator.platform === "Win32";
  };
  qq.android = function(){
      "use strict";
      return navigator.userAgent.toLowerCase().indexOf('android') !== -1;
  };
  
  //
  // Events
  
  qq.preventDefault = function(e){
      "use strict";
      if (e.preventDefault){
          e.preventDefault();
      } else{
          e.returnValue = false;
      }
  };
  
  /**
   * Creates and returns element from html string
   * Uses innerHTML to create an element
   */
  qq.toElement = (function(){
      "use strict";
      var div = document.createElement('div');
      return function(html){
          div.innerHTML = html;
          var element = div.firstChild;
          div.removeChild(element);
          return element;
      };
  }());
  
  //key and value are passed to callback for each item in the object or array
  qq.each = function(obj, callback) {
      "use strict";
      var key, retVal;
      if (obj) {
          for (key in obj) {
              if (Object.prototype.hasOwnProperty.call(obj, key)) {
                  retVal = callback(key, obj[key]);
                  if (retVal === false) {
                      break;
                  }
              }
          }
      }
  };
  
  /**
   * 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){
      "use strict";
      /*jshint laxbreak: true*/
       var i, len,
           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 (i = -1, len = obj.length; i < len; i+=1){
              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 (i in obj){
              if (obj.hasOwnProperty(i)) {
                  add(obj[i], i);
              }
          }
      } else {
          uristrings.push(encodeURIComponent(temp) + '=' + encodeURIComponent(obj));
      }
  
      if (temp) {
          return uristrings.join(prefix);
      } else {
          return uristrings.join(prefix)
              .replace(/^&/, '')
              .replace(/%20/g, '+');
      }
  };
  
  qq.obj2FormData = function(obj, formData, arrayKeyName) {
      "use strict";
      if (!formData) {
          formData = new FormData();
      }
  
      qq.each(obj, function(key, val) {
          key = arrayKeyName ? arrayKeyName + '[' + key + ']' : key;
  
          if (qq.isObject(val)) {
              qq.obj2FormData(val, formData, key);
          }
          else if (qq.isFunction(val)) {
              formData.append(key, val());
          }
          else {
              formData.append(key, val);
          }
      });
  
      return formData;
  };
  
  qq.obj2Inputs = function(obj, form) {
      "use strict";
      var input;
  
      if (!form) {
          form = document.createElement('form');
      }
  
      qq.obj2FormData(obj, {
          append: function(key, val) {
              input = document.createElement('input');
              input.setAttribute('name', key);
              input.setAttribute('value', val);
              form.appendChild(input);
          }
      });
  
      return form;
  };
  
  qq.setCookie = function(name, value, days) {
      var date = new Date(),
          expires = "";
  
  	if (days) {
  		date.setTime(date.getTime()+(days*24*60*60*1000));
  		expires = "; expires="+date.toGMTString();
  	}
  
  	document.cookie = name+"="+value+expires+"; path=/";
  };
  
  qq.getCookie = function(name) {
  	var nameEQ = name + "=",
          ca = document.cookie.split(';'),
          c;
  
  	for(var i=0;i < ca.length;i++) {
  		c = ca[i];
  		while (c.charAt(0)==' ') {
              c = c.substring(1,c.length);
          }
  		if (c.indexOf(nameEQ) === 0) {
              return c.substring(nameEQ.length,c.length);
          }
  	}
  };
  
  qq.getCookieNames = function(regexp) {
      var cookies = document.cookie.split(';'),
          cookieNames = [];
  
      qq.each(cookies, function(idx, cookie) {
          cookie = qq.trimStr(cookie);
  
          var equalsIdx = cookie.indexOf("=");
  
          if (cookie.match(regexp)) {
              cookieNames.push(cookie.substr(0, equalsIdx));
          }
      });
  
      return cookieNames;
  };
  
  qq.deleteCookie = function(name) {
  	qq.setCookie(name, "", -1);
  };
  
  qq.areCookiesEnabled = function() {
      var randNum = Math.random() * 100000,
          name = "qqCookieTest:" + randNum;
      qq.setCookie(name, 1);
  
      if (qq.getCookie(name)) {
          qq.deleteCookie(name);
          return true;
      }
      return false;
  };
  
  /**
   * Not recommended for use outside of Fine Uploader since this falls back to an unchecked eval if JSON.parse is not
   * implemented.  For a more secure JSON.parse polyfill, use Douglas Crockford's json2.js.
   */
  qq.parseJson = function(json) {
      /*jshint evil: true*/
      if (window.JSON && qq.isFunction(JSON.parse)) {
          return JSON.parse(json);
      } else {
          return eval("(" + json + ")");
      }
  };
  
  /**
   * A generic module which supports object disposing in dispose() method.
   * */
  qq.DisposeSupport = function() {
      "use strict";
      var disposers = [];
  
      return {
          /** Run all registered disposers */
          dispose: function() {
              var disposer;
              do {
                  disposer = disposers.shift();
                  if (disposer) {
                      disposer();
                  }
              }
              while (disposer);
          },
  
          /** Attach event handler and register de-attacher as a disposer */
          attach: function() {
              var args = arguments;
              /*jslint undef:true*/
              this.addDisposer(qq(args[0]).attach.apply(this, Array.prototype.slice.call(arguments, 1)));
          },
  
          /** Add disposer to the collection */
          addDisposer: function(disposeFunction) {
              disposers.push(disposeFunction);
          }
      };
  };
  /*globals qq*/
  qq.Promise = function() {
      "use strict";
  
      var successValue, failureValue,
          successCallback, failureCallback,
          state = 0;
  
      return {
          then: function(onSuccess, onFailure) {
              if (state === 0) {
                  successCallback = onSuccess;
                  failureCallback = onFailure;
              }
              else if (state === -1 && onFailure) {
                  onFailure(failureValue);
              }
              else if (onSuccess) {
                  onSuccess(successValue);
              }
          },
  
          success: function(val) {
              state = 1;
              successValue = val;
  
              if (successCallback) {
                  successCallback(val);
              }
  
              return this;
          },
  
          failure: function(val) {
              state = -1;
              failureValue = val;
  
              if (failureCallback) {
                  failureCallback(val);
              }
  
              return this;
          }
      };
  };
  /*globals qq*/
  qq.UploadButton = function(o) {
      "use strict";
  
      var input,
          disposeSupport = new qq.DisposeSupport(),
          options = {
              element: null,
              // if set to true adds multiple attribute to file input
              multiple: false,
              acceptFiles: null,
              // name attribute of file input
              name: 'file',
              onChange: function(input) {},
              hoverClass: 'qq-upload-button-hover',
              focusClass: 'qq-upload-button-focus'
          };
  
      function createInput() {
          var input = document.createElement("input");
  
          if (options.multiple){
              input.setAttribute("multiple", "multiple");
          }
  
          if (options.acceptFiles) {
              input.setAttribute("accept", options.acceptFiles);
          }
  
          input.setAttribute("type", "file");
          input.setAttribute("name", options.name);
  
          qq(input).css({
              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
          });
  
          options.element.appendChild(input);
  
          disposeSupport.attach(input, 'change', function(){
              options.onChange(input);
          });
  
          disposeSupport.attach(input, 'mouseover', function(){
              qq(options.element).addClass(options.hoverClass);
          });
          disposeSupport.attach(input, 'mouseout', function(){
              qq(options.element).removeClass(options.hoverClass);
          });
          disposeSupport.attach(input, 'focus', function(){
              qq(options.element).addClass(options.focusClass);
          });
          disposeSupport.attach(input, 'blur', function(){
              qq(options.element).removeClass(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;
      }
  
  
      qq.extend(options, o);
  
      // make button suitable container for input
      qq(options.element).css({
          position: 'relative',
          overflow: 'hidden',
          // Make sure browse button is in the right side
          // in Internet Explorer
          direction: 'ltr'
      });
  
      input = createInput();
  
      return {
          getInput: function(){
              return input;
          },
  
          reset: function(){
              if (input.parentNode){
                  qq(input).remove();
              }
  
              qq(options.element).removeClass(options.focusClass);
              input = createInput();
          }
      };
  };
  /*globals qq*/
  qq.PasteSupport = function(o) {
      "use strict";
  
      var options, detachPasteHandler;
  
      options = {
          targetElement: null,
          callbacks: {
              log: function(message, level) {},
              pasteReceived: function(blob) {}
          }
      };
  
      function isImage(item) {
          return item.type &&
              item.type.indexOf("image/") === 0;
      }
  
      function registerPasteHandler() {
          qq(options.targetElement).attach("paste", function(event) {
              var clipboardData = event.clipboardData;
  
              if (clipboardData) {
                  qq.each(clipboardData.items, function(idx, item) {
                      if (isImage(item)) {
                          var blob = item.getAsFile();
                          options.callbacks.pasteReceived(blob);
                      }
                  });
              }
          });
      }
  
      function unregisterPasteHandler() {
          if (detachPasteHandler) {
              detachPasteHandler();
          }
      }
  
      qq.extend(options, o);
      registerPasteHandler();
  
      return {
          reset: function() {
              unregisterPasteHandler();
          }
      };
  };
  qq.FineUploaderBasic = function(o){
      var that = this;
      this._options = {
          debug: false,
          button: null,
          multiple: true,
          maxConnections: 3,
          disableCancelForFormUploads: false,
          autoUpload: true,
          request: {
              endpoint: '/server/upload',
              params: {},
              paramsInBody: true,
              customHeaders: {},
              forceMultipart: true,
              inputName: 'qqfile',
              uuidName: 'qquuid',
              totalFileSizeName: 'qqtotalfilesize'
          },
          validation: {
              allowedExtensions: [],
              sizeLimit: 0,
              minSizeLimit: 0,
              itemLimit: 0,
              stopOnFirstInvalidFile: true
          },
          callbacks: {
              onSubmit: function(id, name){},
              onSubmitted: function(id, name){},
              onComplete: function(id, name, responseJSON){},
              onCancel: function(id, name){},
              onUpload: function(id, name){},
              onUploadChunk: function(id, name, chunkData){},
              onResume: function(id, fileName, chunkData){},
              onProgress: function(id, name, loaded, total){},
              onError: function(id, name, reason, maybeXhr) {},
              onAutoRetry: function(id, name, attemptNumber) {},
              onManualRetry: function(id, name) {},
              onValidateBatch: function(fileOrBlobData) {},
              onValidate: function(fileOrBlobData) {},
              onSubmitDelete: function(id) {},
              onDelete: function(id){},
              onDeleteComplete: function(id, xhr, isError){},
              onPasteReceived: function(blob) {
                  return new qq.Promise().success();
              }
          },
          messages: {
              typeError: "{file} has an invalid extension. Valid extension(s): {extensions}.",
              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.",
              noFilesError: "No files to upload.",
              tooManyItemsError: "Too many items ({netItems}) would be uploaded.  Item limit is {itemLimit}.",
              retryFailTooManyItems: "Retry failed - you have reached your file limit.",
              onLeave: "The files are being uploaded, if you leave now the upload will be cancelled."
          },
          retry: {
              enableAuto: false,
              maxAutoAttempts: 3,
              autoAttemptDelay: 5,
              preventRetryResponseProperty: 'preventRetry'
          },
          classes: {
              buttonHover: 'qq-upload-button-hover',
              buttonFocus: 'qq-upload-button-focus'
          },
          chunking: {
              enabled: false,
              partSize: 2000000,
              paramNames: {
                  partIndex: 'qqpartindex',
                  partByteOffset: 'qqpartbyteoffset',
                  chunkSize: 'qqchunksize',
                  totalFileSize: 'qqtotalfilesize',
                  totalParts: 'qqtotalparts',
                  filename: 'qqfilename'
              }
          },
          resume: {
              enabled: false,
              id: null,
              cookiesExpireIn: 7, //days
              paramNames: {
                  resuming: "qqresume"
              }
          },
          formatFileName: function(fileOrBlobName) {
              if (fileOrBlobName.length > 33) {
                  fileOrBlobName = fileOrBlobName.slice(0, 19) + '...' + fileOrBlobName.slice(-14);
              }
              return fileOrBlobName;
          },
          text: {
              sizeSymbols: ['kB', 'MB', 'GB', 'TB', 'PB', 'EB']
          },
          deleteFile : {
              enabled: false,
              endpoint: '/server/upload',
              customHeaders: {},
              params: {}
          },
          cors: {
              expected: false,
              sendCredentials: false
          },
          blobs: {
              defaultName: 'misc_data',
              paramNames: {
                  name: 'qqblobname'
              }
          },
          paste: {
              targetElement: null,
              defaultName: 'pasted_image'
          }
      };
  
      qq.extend(this._options, o, true);
      this._wrapCallbacks();
      this._disposeSupport =  new qq.DisposeSupport();
  
      this._filesInProgress = [];
      this._storedIds = [];
      this._autoRetries = [];
      this._retryTimeouts = [];
      this._preventRetries = [];
      this._netFilesUploadedOrQueued = 0;
  
      this._paramsStore = this._createParamsStore("request");
      this._deleteFileParamsStore = this._createParamsStore("deleteFile");
  
      this._endpointStore = this._createEndpointStore("request");
      this._deleteFileEndpointStore = this._createEndpointStore("deleteFile");
  
      this._handler = this._createUploadHandler();
      this._deleteHandler = this._createDeleteHandler();
  
      if (this._options.button){
          this._button = this._createUploadButton(this._options.button);
      }
  
      if (this._options.paste.targetElement) {
          this._pasteHandler = this._createPasteHandler();
      }
  
      this._preventLeaveInProgress();
  };
  
  qq.FineUploaderBasic.prototype = {
      log: function(str, level) {
          if (this._options.debug && (!level || level === 'info')) {
              qq.log('[FineUploader] ' + str);
          }
          else if (level && level !== 'info') {
              qq.log('[FineUploader] ' + str, level);
  
          }
      },
      setParams: function(params, id) {
          /*jshint eqeqeq: true, eqnull: true*/
          if (id == null) {
              this._options.request.params = params;
          }
          else {
              this._paramsStore.setParams(params, id);
          }
      },
      setDeleteFileParams: function(params, id) {
          /*jshint eqeqeq: true, eqnull: true*/
          if (id == null) {
              this._options.deleteFile.params = params;
          }
          else {
              this._deleteFileParamsStore.setParams(params, id);
          }
      },
      setEndpoint: function(endpoint, id) {
          /*jshint eqeqeq: true, eqnull: true*/
          if (id == null) {
              this._options.request.endpoint = endpoint;
          }
          else {
              this._endpointStore.setEndpoint(endpoint, id);
          }
      },
      getInProgress: function(){
          return this._filesInProgress.length;
      },
      uploadStoredFiles: function(){
          "use strict";
          var idToUpload;
  
          while(this._storedIds.length) {
              idToUpload = this._storedIds.shift();
              this._filesInProgress.push(idToUpload);
              this._handler.upload(idToUpload);
          }
      },
      clearStoredFiles: function(){
          this._storedIds = [];
      },
      retry: function(id) {
          if (this._onBeforeManualRetry(id)) {
              this._netFilesUploadedOrQueued++;
              this._handler.retry(id);
              return true;
          }
          else {
              return false;
          }
      },
      cancel: function(id) {
          this._handler.cancel(id);
      },
      cancelAll: function() {
          var storedIdsCopy = [],
              self = this;
  
          qq.extend(storedIdsCopy, this._storedIds);
          qq.each(storedIdsCopy, function(idx, storedFileId) {
              self.cancel(storedFileId);
          });
  
          this._handler.cancelAll();
      },
      reset: function() {
          this.log("Resetting uploader...");
          this._handler.reset();
          this._filesInProgress = [];
          this._storedIds = [];
          this._autoRetries = [];
          this._retryTimeouts = [];
          this._preventRetries = [];
          this._button.reset();
          this._paramsStore.reset();
          this._endpointStore.reset();
          this._netFilesUploadedOrQueued = 0;
  
          if (this._pasteHandler) {
              this._pasteHandler.reset();
          }
      },
      addFiles: function(filesBlobDataOrInputs) {
          var self = this,
              verifiedFilesOrInputs = [],
              index, fileOrInput;
  
          if (filesBlobDataOrInputs) {
              if (!window.FileList || !(filesBlobDataOrInputs instanceof FileList)) {
                  filesBlobDataOrInputs = [].concat(filesBlobDataOrInputs);
              }
  
              for (index = 0; index < filesBlobDataOrInputs.length; index+=1) {
                  fileOrInput = filesBlobDataOrInputs[index];
  
                  if (qq.isFileOrInput(fileOrInput)) {
                      verifiedFilesOrInputs.push(fileOrInput);
                  }
                  else {
                      self.log(fileOrInput + ' is not a File or INPUT element!  Ignoring!', 'warn');
                  }
              }
  
              this.log('Processing ' + verifiedFilesOrInputs.length + ' files or inputs...');
              this._uploadFileOrBlobDataList(verifiedFilesOrInputs);
          }
      },
      addBlobs: function(blobDataOrArray) {
          if (blobDataOrArray) {
              var blobDataArray = [].concat(blobDataOrArray),
                  verifiedBlobDataList = [],
                  self = this;
  
              qq.each(blobDataArray, function(idx, blobData) {
                  if (qq.isBlob(blobData) && !qq.isFileOrInput(blobData)) {
                      verifiedBlobDataList.push({
                          blob: blobData,
                          name: self._options.blobs.defaultName
                      });
                  }
                  else if (qq.isObject(blobData) && blobData.blob && blobData.name) {
                      verifiedBlobDataList.push(blobData);
                  }
                  else {
                      self.log("addBlobs: entry at index " + idx + " is not a Blob or a BlobData object", "error");
                  }
              });
  
              this._uploadFileOrBlobDataList(verifiedBlobDataList);
          }
          else {
              this.log("undefined or non-array parameter passed into addBlobs", "error");
          }
      },
      getUuid: function(id) {
          return this._handler.getUuid(id);
      },
      getResumableFilesData: function() {
          return this._handler.getResumableFilesData();
      },
      getSize: function(id) {
          return this._handler.getSize(id);
      },
      getName: function(id) {
          return this._handler.getName(id);
      },
      getFile: function(fileOrBlobId) {
          return this._handler.getFile(fileOrBlobId);
      },
      deleteFile: function(id) {
          this._onSubmitDelete(id);
      },
      setDeleteFileEndpoint: function(endpoint, id) {
          /*jshint eqeqeq: true, eqnull: true*/
          if (id == null) {
              this._options.deleteFile.endpoint = endpoint;
          }
          else {
              this._deleteFileEndpointStore.setEndpoint(endpoint, id);
          }
      },
      getPromissoryCallbackNames: function() {
          return ["onPasteReceived"];
      },
      _createUploadButton: function(element){
          var self = this;
  
          var button = new qq.UploadButton({
              element: element,
              multiple: this._options.multiple && qq.isXhrUploadSupported(),
              acceptFiles: this._options.validation.acceptFiles,
              onChange: function(input){
                  self._onInputChange(input);
              },
              hoverClass: this._options.classes.buttonHover,
              focusClass: this._options.classes.buttonFocus
          });
  
          this._disposeSupport.addDisposer(function() { button.dispose(); });
          return button;
      },
      _createUploadHandler: function(){
          var self = this;
  
          return new qq.UploadHandler({
              debug: this._options.debug,
              forceMultipart: this._options.request.forceMultipart,
              maxConnections: this._options.maxConnections,
              customHeaders: this._options.request.customHeaders,
              inputName: this._options.request.inputName,
              uuidParamName: this._options.request.uuidName,
              totalFileSizeParamName: this._options.request.totalFileSizeName,
              cors: this._options.cors,
              demoMode: this._options.demoMode,
              paramsInBody: this._options.request.paramsInBody,
              paramsStore: this._paramsStore,
              endpointStore: this._endpointStore,
              chunking: this._options.chunking,
              resume: this._options.resume,
              blobs: this._options.blobs,
              log: function(str, level) {
                  self.log(str, level);
              },
              onProgress: function(id, name, loaded, total){
                  self._onProgress(id, name, loaded, total);
                  self._options.callbacks.onProgress(id, name, loaded, total);
              },
              onComplete: function(id, name, result, xhr){
                  self._onComplete(id, name, result, xhr);
                  self._options.callbacks.onComplete(id, name, result);
              },
              onCancel: function(id, name){
                  self._onCancel(id, name);
                  self._options.callbacks.onCancel(id, name);
              },
              onUpload: function(id, name){
                  self._onUpload(id, name);
                  self._options.callbacks.onUpload(id, name);
              },
              onUploadChunk: function(id, name, chunkData){
                  self._options.callbacks.onUploadChunk(id, name, chunkData);
              },
              onResume: function(id, name, chunkData) {
                  return self._options.callbacks.onResume(id, name, chunkData);
              },
              onAutoRetry: function(id, name, responseJSON, xhr) {
                  self._preventRetries[id] = responseJSON[self._options.retry.preventRetryResponseProperty];
  
                  if (self._shouldAutoRetry(id, name, responseJSON)) {
                      self._maybeParseAndSendUploadError(id, name, responseJSON, xhr);
                      self._options.callbacks.onAutoRetry(id, name, self._autoRetries[id] + 1);
                      self._onBeforeAutoRetry(id, name);
  
                      self._retryTimeouts[id] = setTimeout(function() {
                          self._onAutoRetry(id, name, responseJSON)
                      }, self._options.retry.autoAttemptDelay * 1000);
  
                      return true;
                  }
                  else {
                      return false;
                  }
              }
          });
      },
      _createDeleteHandler: function() {
          var self = this;
  
          return new qq.DeleteFileAjaxRequestor({
              maxConnections: this._options.maxConnections,
              customHeaders: this._options.deleteFile.customHeaders,
              paramsStore: this._deleteFileParamsStore,
              endpointStore: this._deleteFileEndpointStore,
              demoMode: this._options.demoMode,
              cors: this._options.cors,
              log: function(str, level) {
                  self.log(str, level);
              },
              onDelete: function(id) {
                  self._onDelete(id);
                  self._options.callbacks.onDelete(id);
              },
              onDeleteComplete: function(id, xhr, isError) {
                  self._onDeleteComplete(id, xhr, isError);
                  self._options.callbacks.onDeleteComplete(id, xhr, isError);
              }
  
          });
      },
      _createPasteHandler: function() {
          var self = this;
  
          return new qq.PasteSupport({
              targetElement: this._options.paste.targetElement,
              callbacks: {
                  log: function(str, level) {
                      self.log(str, level);
                  },
                  pasteReceived: function(blob) {
                      var pasteReceivedCallback = self._options.callbacks.onPasteReceived,
                          promise = pasteReceivedCallback(blob);
  
                      if (promise.then) {
                          promise.then(function(successData) {
                              self._handlePasteSuccess(blob, successData);
                          }, function(failureData) {
                              self.log("Ignoring pasted image per paste received callback.  Reason = '" + failureData + "'");
                          });
                      }
                      else {
                          self.log("Promise contract not fulfilled in pasteReceived callback handler!  Ignoring pasted item.", "error");
                      }
                  }
              }
          });
      },
      _handlePasteSuccess: function(blob, extSuppliedName) {
          var extension = blob.type.split("/")[1],
              name = extSuppliedName;
  
          /*jshint eqeqeq: true, eqnull: true*/
          if (name == null) {
              name = this._options.paste.defaultName;
          }
  
          name += '.' + extension;
  
          this.addBlobs({
              name: name,
              blob: blob
          });
      },
      _preventLeaveInProgress: function(){
          var self = this;
  
          this._disposeSupport.attach(window, 'beforeunload', function(e){
              if (!self._filesInProgress.length){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, name) {
          this._netFilesUploadedOrQueued++;
  
          if (this._options.autoUpload) {
              this._filesInProgress.push(id);
          }
      },
      _onProgress: function(id, name, loaded, total){
      },
      _onComplete: function(id, name, result, xhr) {
          if (!result.success) {
              this._netFilesUploadedOrQueued--;
          }
  
          this._removeFromFilesInProgress(id);
          this._maybeParseAndSendUploadError(id, name, result, xhr);
      },
      _onCancel: function(id, name){
          this._netFilesUploadedOrQueued--;
  
          this._removeFromFilesInProgress(id);
  
          clearTimeout(this._retryTimeouts[id]);
  
          var storedItemIndex = qq.indexOf(this._storedIds, id);
          if (!this._options.autoUpload && storedItemIndex >= 0) {
              this._storedIds.splice(storedItemIndex, 1);
          }
      },
      _isDeletePossible: function() {
          return (this._options.deleteFile.enabled &&
              (!this._options.cors.expected ||
                  (this._options.cors.expected && (qq.ie10() || !qq.ie()))
                  )
              );
      },
      _onSubmitDelete: function(id) {
          if (this._isDeletePossible()) {
              if (this._options.callbacks.onSubmitDelete(id) !== false) {
                  this._deleteHandler.sendDelete(id, this.getUuid(id));
              }
          }
          else {
              this.log("Delete request ignored for ID " + id + ", delete feature is disabled or request not possible " +
                  "due to CORS on a user agent that does not support pre-flighting.", "warn");
              return false;
          }
      },
      _onDelete: function(fileId) {},
      _onDeleteComplete: function(id, xhr, isError) {
          var name = this._handler.getName(id);
  
          if (isError) {
              this.log("Delete request for '" + name + "' has failed.", "error");
              this._options.callbacks.onError(id, name, "Delete request failed with response code " + xhr.status, xhr);
          }
          else {
              this._netFilesUploadedOrQueued--;
              this.log("Delete request for '" + name + "' has succeeded.");
          }
      },
      _removeFromFilesInProgress: function(id) {
          var index = qq.indexOf(this._filesInProgress, id);
          if (index >= 0) {
              this._filesInProgress.splice(index, 1);
          }
      },
      _onUpload: function(id, name){},
      _onInputChange: function(input){
          if (qq.isXhrUploadSupported()){
              this.addFiles(input.files);
          } else {
              this.addFiles(input);
          }
          this._button.reset();
      },
      _onBeforeAutoRetry: function(id, name) {
          this.log("Waiting " + this._options.retry.autoAttemptDelay + " seconds before retrying " + name + "...");
      },
      _onAutoRetry: function(id, name, responseJSON) {
          this.log("Retrying " + name + "...");
          this._autoRetries[id]++;
          this._handler.retry(id);
      },
      _shouldAutoRetry: function(id, name, responseJSON) {
          if (!this._preventRetries[id] && this._options.retry.enableAuto) {
              if (this._autoRetries[id] === undefined) {
                  this._autoRetries[id] = 0;
              }
  
              return this._autoRetries[id] < this._options.retry.maxAutoAttempts;
          }
  
          return false;
      },
      //return false if we should not attempt the requested retry
      _onBeforeManualRetry: function(id) {
          var itemLimit = this._options.validation.itemLimit;
  
          if (this._preventRetries[id]) {
              this.log("Retries are forbidden for id " + id, 'warn');
              return false;
          }
          else if (this._handler.isValid(id)) {
              var fileName = this._handler.getName(id);
  
              if (this._options.callbacks.onManualRetry(id, fileName) === false) {
                  return false;
              }
  
              if (itemLimit > 0 && this._netFilesUploadedOrQueued+1 > itemLimit) {
                  this._itemError("retryFailTooManyItems", "");
                  return false;
              }
  
              this.log("Retrying upload for '" + fileName + "' (id: " + id + ")...");
              this._filesInProgress.push(id);
              return true;
          }
          else {
              this.log("'" + id + "' is not a valid file ID", 'error');
              return false;
          }
      },
      _maybeParseAndSendUploadError: function(id, name, response, xhr) {
          //assuming no one will actually set the response code to something other than 200 and still set 'success' to true
          if (!response.success){
              if (xhr && xhr.status !== 200 && !response.error) {
                  this._options.callbacks.onError(id, name, "XHR returned response code " + xhr.status, xhr);
              }
              else {
                  var errorReason = response.error ? response.error : "Upload failure reason unknown";
                  this._options.callbacks.onError(id, name, errorReason, xhr);
              }
          }
      },
      _uploadFileOrBlobDataList: function(fileOrBlobDataList){
          var index,
              validationDescriptors = this._getValidationDescriptors(fileOrBlobDataList),
              batchValid = this._isBatchValid(validationDescriptors);
  
          if (batchValid) {
              if (fileOrBlobDataList.length > 0) {
                  for (index = 0; index < fileOrBlobDataList.length; index++){
                      if (this._validateFileOrBlobData(fileOrBlobDataList[index])){
                          this._upload(fileOrBlobDataList[index]);
                      } else {
                          if (this._options.validation.stopOnFirstInvalidFile){
                              return;
                          }
                      }
                  }
              }
              else {
                  this._itemError("noFilesError", "");
              }
          }
      },
      _upload: function(blobOrFileContainer){
          var id = this._handler.add(blobOrFileContainer);
          var name = this._handler.getName(id);
  
          if (this._options.callbacks.onSubmit(id, name) !== false) {
              this._onSubmit(id, name);
              this._options.callbacks.onSubmitted(id, name);
  
              if (this._options.autoUpload) {
                  this._handler.upload(id);
              }
              else {
                  this._storeForLater(id);
              }
          }
      },
      _storeForLater: function(id) {
          this._storedIds.push(id);
      },
      _isBatchValid: function(validationDescriptors) {
          //first, defer the check to the callback (ask the integrator)
          var errorMessage,
              itemLimit = this._options.validation.itemLimit,
              proposedNetFilesUploadedOrQueued = this._netFilesUploadedOrQueued + validationDescriptors.length,
              batchValid = this._options.callbacks.onValidateBatch(validationDescriptors) !== false;
  
          //if the callback hasn't rejected the batch, run some internal tests on the batch next
          if (batchValid) {
              if (itemLimit === 0 || proposedNetFilesUploadedOrQueued <= itemLimit) {
                  batchValid = true;
              }
              else {
                  batchValid = false;
                  errorMessage = this._options.messages.tooManyItemsError
                      .replace(/\{netItems\}/g, proposedNetFilesUploadedOrQueued)
                      .replace(/\{itemLimit\}/g, itemLimit);
                  this._batchError(errorMessage);
              }
          }
  
          return batchValid;
      },
      _validateFileOrBlobData: function(fileOrBlobData){
          var validationDescriptor, name, size;
  
          validationDescriptor = this._getValidationDescriptor(fileOrBlobData);
          name = validationDescriptor.name;
          size = validationDescriptor.size;
  
          if (this._options.callbacks.onValidate(validationDescriptor) === false) {
              return false;
          }
  
          if (qq.isFileOrInput(fileOrBlobData) && !this._isAllowedExtension(name)){
              this._itemError('typeError', name);
              return false;
  
          }
          else if (size === 0){
              this._itemError('emptyError', name);
              return false;
  
          }
          else if (size && this._options.validation.sizeLimit && size > this._options.validation.sizeLimit){
              this._itemError('sizeError', name);
              return false;
  
          }
          else if (size && size < this._options.validation.minSizeLimit){
              this._itemError('minSizeError', name);
              return false;
          }
  
          return true;
      },
      _itemError: function(code, name) {
          var message = this._options.messages[code],
              allowedExtensions = [],
              extensionsForMessage;
  
          function r(name, replacement){ message = message.replace(name, replacement); }
  
          qq.each(this._options.validation.allowedExtensions, function(idx, allowedExtension) {
                  /**
                   * If an argument is not a string, ignore it.  Added when a possible issue with MooTools hijacking the
                   * `allowedExtensions` array was discovered.  See case #735 in the issue tracker for more details.
                   */
                  if (qq.isString(allowedExtension)) {
                      allowedExtensions.push(allowedExtension);
                  }
          });
  
          extensionsForMessage = allowedExtensions.join(', ').toLowerCase();
  
          r('{file}', this._options.formatFileName(name));
          r('{extensions}', extensionsForMessage);
          r('{sizeLimit}', this._formatSize(this._options.validation.sizeLimit));
          r('{minSizeLimit}', this._formatSize(this._options.validation.minSizeLimit));
  
          this._options.callbacks.onError(null, name, message);
  
          return message;
      },
      _batchError: function(message) {
          this._options.callbacks.onError(null, null, message);
      },
      _isAllowedExtension: function(fileName){
          var allowed = this._options.validation.allowedExtensions,
              valid = false;
  
          if (!allowed.length) {
              return true;
          }
  
          qq.each(allowed, function(idx, allowedExt) {
              /**
               * If an argument is not a string, ignore it.  Added when a possible issue with MooTools hijacking the
               * `allowedExtensions` array was discovered.  See case #735 in the issue tracker for more details.
               */
              if (qq.isString(allowedExt)) {
                  /*jshint eqeqeq: true, eqnull: true*/
                  var extRegex = new RegExp('\\.' + allowedExt + "$", 'i');
  
                  if (fileName.match(extRegex) != null) {
                      valid = true;
                      return false;
                  }
              }
          });
  
          return valid;
      },
      _formatSize: function(bytes){
          var i = -1;
          do {
              bytes = bytes / 1024;
              i++;
          } while (bytes > 99);
  
          return Math.max(bytes, 0.1).toFixed(1) + this._options.text.sizeSymbols[i];
      },
      _wrapCallbacks: function() {
          var self, safeCallback;
  
          self = this;
  
          safeCallback = function(name, callback, args) {
              try {
                  return callback.apply(self, args);
              }
              catch (exception) {
                  self.log("Caught exception in '" + name + "' callback - " + exception.message, 'error');
              }
          };
  
          for (var prop in this._options.callbacks) {
              (function() {
                  var callbackName, callbackFunc;
                  callbackName = prop;
                  callbackFunc = self._options.callbacks[callbackName];
                  self._options.callbacks[callbackName] = function() {
                      return safeCallback(callbackName, callbackFunc, arguments);
                  };
              }());
          }
      },
      _parseFileOrBlobDataName: function(fileOrBlobData) {
          var name;
  
          if (qq.isFileOrInput(fileOrBlobData)) {
              if (fileOrBlobData.value) {
                  // it is a file input
                  // get input value and remove path to normalize
                  name = fileOrBlobData.value.replace(/.*(\/|\\)/, "");
              } else {
                  // fix missing properties in Safari 4 and firefox 11.0a2
                  name = (fileOrBlobData.fileName !== null && fileOrBlobData.fileName !== undefined) ? fileOrBlobData.fileName : fileOrBlobData.name;
              }
          }
          else {
              name = fileOrBlobData.name;
          }
  
          return name;
      },
      _parseFileOrBlobDataSize: function(fileOrBlobData) {
          var size;
  
          if (qq.isFileOrInput(fileOrBlobData)) {
              if (!fileOrBlobData.value){
                  // fix missing properties in Safari 4 and firefox 11.0a2
                  size = (fileOrBlobData.fileSize !== null && fileOrBlobData.fileSize !== undefined) ? fileOrBlobData.fileSize : fileOrBlobData.size;
              }
          }
          else {
              size = fileOrBlobData.blob.size;
          }
  
          return size;
      },
      _getValidationDescriptor: function(fileOrBlobData) {
          var name, size, fileDescriptor;
  
          fileDescriptor = {};
          name = this._parseFileOrBlobDataName(fileOrBlobData);
          size = this._parseFileOrBlobDataSize(fileOrBlobData);
  
          fileDescriptor.name = name;
          if (size) {
              fileDescriptor.size = size;
          }
  
          return fileDescriptor;
      },
      _getValidationDescriptors: function(files) {
          var self = this,
              fileDescriptors = [];
  
          qq.each(files, function(idx, file) {
              fileDescriptors.push(self._getValidationDescriptor(file));
          });
  
          return fileDescriptors;
      },
      _createParamsStore: function(type) {
          var paramsStore = {},
              self = this;
  
          return {
              setParams: function(params, id) {
                  var paramsCopy = {};
                  qq.extend(paramsCopy, params);
                  paramsStore[id] = paramsCopy;
              },
  
              getParams: function(id) {
                  /*jshint eqeqeq: true, eqnull: true*/
                  var paramsCopy = {};
  
                  if (id != null && paramsStore[id]) {
                      qq.extend(paramsCopy, paramsStore[id]);
                  }
                  else {
                      qq.extend(paramsCopy, self._options[type].params);
                  }
  
                  return paramsCopy;
              },
  
              remove: function(fileId) {
                  return delete paramsStore[fileId];
              },
  
              reset: function() {
                  paramsStore = {};
              }
          };
      },
      _createEndpointStore: function(type) {
          var endpointStore = {},
          self = this;
  
          return {
              setEndpoint: function(endpoint, id) {
                  endpointStore[id] = endpoint;
              },
  
              getEndpoint: function(id) {
                  /*jshint eqeqeq: true, eqnull: true*/
                  if (id != null && endpointStore[id]) {
                      return endpointStore[id];
                  }
  
                  return self._options[type].endpoint;
              },
  
              remove: function(fileId) {
                  return delete endpointStore[fileId];
              },
  
              reset: function() {
                  endpointStore = {};
              }
          };
      }
  };
  /*globals qq, document*/
  qq.DragAndDrop = function(o) {
      "use strict";
  
      var options, dz, dirPending,
          droppedFiles = [],
          droppedEntriesCount = 0,
          droppedEntriesParsedCount = 0,
          disposeSupport = new qq.DisposeSupport();
  
       options = {
          dropArea: null,
          extraDropzones: [],
          hideDropzones: true,
          multiple: true,
          classes: {
              dropActive: null
          },
          callbacks: {
              dropProcessing: function(isProcessing, files) {},
              error: function(code, filename) {},
              log: function(message, level) {}
          }
      };
  
      qq.extend(options, o);
  
      function maybeUploadDroppedFiles() {
          if (droppedEntriesCount === droppedEntriesParsedCount && !dirPending) {
              options.callbacks.log('Grabbed ' + droppedFiles.length + " files after tree traversal.");
              dz.dropDisabled(false);
              options.callbacks.dropProcessing(false, droppedFiles);
          }
      }
      function addDroppedFile(file) {
          droppedFiles.push(file);
          droppedEntriesParsedCount+=1;
          maybeUploadDroppedFiles();
      }
  
      function traverseFileTree(entry) {
          var dirReader, i;
  
          droppedEntriesCount+=1;
  
          if (entry.isFile) {
              entry.file(function(file) {
                  addDroppedFile(file);
              });
          }
          else if (entry.isDirectory) {
              dirPending = true;
              dirReader = entry.createReader();
              dirReader.readEntries(function(entries) {
                  droppedEntriesParsedCount+=1;
                  for (i = 0; i < entries.length; i+=1) {
                      traverseFileTree(entries[i]);
                  }
  
                  dirPending = false;
  
                  if (!entries.length) {
                      maybeUploadDroppedFiles();
                  }
              });
          }
      }
  
      function handleDataTransfer(dataTransfer) {
          var i, items, entry;
  
          options.callbacks.dropProcessing(true);
          dz.dropDisabled(true);
  
          if (dataTransfer.files.length > 1 && !options.multiple) {
              options.callbacks.dropProcessing(false);
              options.callbacks.error('tooManyFilesError', "");
              dz.dropDisabled(false);
          }
          else {
              droppedFiles = [];
              droppedEntriesCount = 0;
              droppedEntriesParsedCount = 0;
  
              if (qq.isFolderDropSupported(dataTransfer)) {
                  items = dataTransfer.items;
  
                  for (i = 0; i < items.length; i+=1) {
                      entry = items[i].webkitGetAsEntry();
                      if (entry) {
                          //due to a bug in Chrome's File System API impl - #149735
                          if (entry.isFile) {
                              droppedFiles.push(items[i].getAsFile());
                              if (i === items.length-1) {
                                  maybeUploadDroppedFiles();
                              }
                          }
  
                          else {
                              traverseFileTree(entry);
                          }
                      }
                  }
              }
              else {
                  options.callbacks.dropProcessing(false, dataTransfer.files);
                  dz.dropDisabled(false);
              }
          }
      }
  
      function setupDropzone(dropArea){
          dz = new qq.UploadDropZone({
              element: dropArea,
              onEnter: function(e){
                  qq(dropArea).addClass(options.classes.dropActive);
                  e.stopPropagation();
              },
              onLeaveNotDescendants: function(e){
                  qq(dropArea).removeClass(options.classes.dropActive);
              },
              onDrop: function(e){
                  if (options.hideDropzones) {
                      qq(dropArea).hide();
                  }
                  qq(dropArea).removeClass(options.classes.dropActive);
  
                  handleDataTransfer(e.dataTransfer);
              }
          });
  
          disposeSupport.addDisposer(function() {
              dz.dispose();
          });
  
          if (options.hideDropzones) {
              qq(dropArea).hide();
          }
      }
  
      function isFileDrag(dragEvent) {
          var fileDrag;
  
          qq.each(dragEvent.dataTransfer.types, function(key, val) {
              if (val === 'Files') {
                  fileDrag = true;
                  return false;
              }
          });
  
          return fileDrag;
      }
  
      function setupDragDrop(){
          if (options.dropArea) {
              options.extraDropzones.push(options.dropArea);
          }
  
          var i, dropzones = options.extraDropzones;
  
          for (i=0; i < dropzones.length; i+=1){
              setupDropzone(dropzones[i]);
          }
  
          // IE <= 9 does not support the File API used for drag+drop uploads
          if (options.dropArea && (!qq.ie() || qq.ie10())) {
              disposeSupport.attach(document, 'dragenter', function(e) {
                  if (!dz.dropDisabled() && isFileDrag(e)) {
                      if (qq(options.dropArea).hasClass(options.classes.dropDisabled)) {
                          return;
                      }
  
                      options.dropArea.style.display = 'block';
                      for (i=0; i < dropzones.length; i+=1) {
                          dropzones[i].style.display = 'block';
                      }
                  }
              });
          }
          disposeSupport.attach(document, 'dragleave', function(e){
              if (options.hideDropzones && qq.FineUploader.prototype._leaving_document_out(e)) {
                  for (i=0; i < dropzones.length; i+=1) {
                      qq(dropzones[i]).hide();
                  }
              }
          });
          disposeSupport.attach(document, 'drop', function(e){
              if (options.hideDropzones) {
                  for (i=0; i < dropzones.length; i+=1) {
                      qq(dropzones[i]).hide();
                  }
              }
              e.preventDefault();
          });
      }
  
      return {
          setup: function() {
              setupDragDrop();
          },
  
          setupExtraDropzone: function(element) {
              options.extraDropzones.push(element);
              setupDropzone(element);
          },
  
          removeExtraDropzone: function(element) {
              var i, dzs = options.extraDropzones;
              for(i in dzs) {
                  if (dzs[i] === element) {
                      return dzs.splice(i, 1);
                  }
              }
          },
  
          dispose: function() {
              disposeSupport.dispose();
              dz.dispose();
          }
      };
  };
  
  
  qq.UploadDropZone = function(o){
      "use strict";
  
      var options, element, preventDrop, dropOutsideDisabled, disposeSupport = new qq.DisposeSupport();
  
      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(options, o);
      element = options.element;
  
      function dragover_should_be_canceled(){
          return qq.safari() || (qq.firefox() && qq.windows());
      }
  
      function disableDropOutside(e){
          // run only once for all instances
          if (!dropOutsideDisabled ){
  
              // for these cases we need to catch onDrop to reset dropArea
              if (dragover_should_be_canceled){
                 disposeSupport.attach(document, 'dragover', function(e){
                      e.preventDefault();
                  });
              } else {
                  disposeSupport.attach(document, 'dragover', function(e){
                      if (e.dataTransfer){
                          e.dataTransfer.dropEffect = 'none';
                          e.preventDefault();
                      }
                  });
              }
  
              dropOutsideDisabled = true;
          }
      }
  
      function isValidFileDrag(e){
          // e.dataTransfer currently causing IE errors
          // IE9 does NOT support file API, so drag-and-drop is not possible
          if (qq.ie() && !qq.ie10()) {
              return false;
          }
  
          var effectTest, dt = e.dataTransfer,
          // do not check dt.types.contains in webkit, because it crashes safari 4
          isSafari = qq.safari();
  
          // dt.effectAllowed is none in Safari 5
          // dt.types.contains check is for firefox
          effectTest = qq.ie10() ? true : dt.effectAllowed !== 'none';
          return dt && effectTest && (dt.files || (!isSafari && dt.types.contains && dt.types.contains('Files')));
      }
  
      function isOrSetDropDisabled(isDisabled) {
          if (isDisabled !== undefined) {
              preventDrop = isDisabled;
          }
          return preventDrop;
      }
  
      function attachEvents(){
          disposeSupport.attach(element, 'dragover', function(e){
              if (!isValidFileDrag(e)) {
                  return;
              }
  
              var effect = qq.ie() ? null : 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();
          });
  
          disposeSupport.attach(element, 'dragenter', function(e){
              if (!isOrSetDropDisabled()) {
                  if (!isValidFileDrag(e)) {
                      return;
                  }
                  options.onEnter(e);
              }
          });
  
          disposeSupport.attach(element, 'dragleave', function(e){
              if (!isValidFileDrag(e)) {
                  return;
              }
  
              options.onLeave(e);
  
              var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);
              // do not fire when moving a mouse over a descendant
              if (qq(this).contains(relatedTarget)) {
                  return;
              }
  
              options.onLeaveNotDescendants(e);
          });
  
          disposeSupport.attach(element, 'drop', function(e){
              if (!isOrSetDropDisabled()) {
                  if (!isValidFileDrag(e)) {
                      return;
                  }
  
                  e.preventDefault();
                  options.onDrop(e);
              }
          });
      }
  
      disableDropOutside();
      attachEvents();
  
      return {
          dropDisabled: function(isDisabled) {
              return isOrSetDropDisabled(isDisabled);
          },
  
          dispose: function() {
              disposeSupport.dispose();
          }
      };
  };
  /**
   * Class that creates upload widget with drag-and-drop and file list
   * @inherits qq.FineUploaderBasic
   */
  qq.FineUploader = function(o){
      // call parent constructor
      qq.FineUploaderBasic.apply(this, arguments);
  
      // additional options
      qq.extend(this._options, {
          element: null,
          listElement: null,
          dragAndDrop: {
              extraDropzones: [],
              hideDropzones: true,
              disableDefaultDropzone: false
          },
          text: {
              uploadButton: 'Upload a file',
              cancelButton: 'Cancel',
              retryButton: 'Retry',
              deleteButton: 'Delete',
              failUpload: 'Upload failed',
              dragZone: 'Drop files here to upload',
              dropProcessing: 'Processing dropped files...',
              formatProgress: "{percent}% of {total_size}",
              waitingForResponse: "Processing..."
          },
          template: '<div class="qq-uploader">' +
              ((!this._options.dragAndDrop || !this._options.dragAndDrop.disableDefaultDropzone) ? '<div class="qq-upload-drop-area"><span>{dragZoneText}</span></div>' : '') +
              (!this._options.button ? '<div class="qq-upload-button"><div>{uploadButtonText}</div></div>' : '') +
              '<span class="qq-drop-processing"><span>{dropProcessingText}</span><span class="qq-drop-processing-spinner"></span></span>' +
              (!this._options.listElement ? '<ul class="qq-upload-list"></ul>' : '') +
              '</div>',
  
          // template for one item in file list
          fileTemplate: '<li>' +
              '<div class="qq-progress-bar"></div>' +
              '<span class="qq-upload-spinner"></span>' +
              '<span class="qq-upload-finished"></span>' +
              '<span class="qq-upload-file"></span>' +
              '<span class="qq-upload-size"></span>' +
              '<a class="qq-upload-cancel" href="#">{cancelButtonText}</a>' +
              '<a class="qq-upload-retry" href="#">{retryButtonText}</a>' +
              '<a class="qq-upload-delete" href="#">{deleteButtonText}</a>' +
              '<span class="qq-upload-status-text">{statusText}</span>' +
              '</li>',
          classes: {
              button: 'qq-upload-button',
              drop: 'qq-upload-drop-area',
              dropActive: 'qq-upload-drop-area-active',
              dropDisabled: 'qq-upload-drop-area-disabled',
              list: 'qq-upload-list',
              progressBar: 'qq-progress-bar',
              file: 'qq-upload-file',
              spinner: 'qq-upload-spinner',
              finished: 'qq-upload-finished',
              retrying: 'qq-upload-retrying',
              retryable: 'qq-upload-retryable',
              size: 'qq-upload-size',
              cancel: 'qq-upload-cancel',
              deleteButton: 'qq-upload-delete',
              retry: 'qq-upload-retry',
              statusText: 'qq-upload-status-text',
  
              success: 'qq-upload-success',
              fail: 'qq-upload-fail',
  
              successIcon: null,
              failIcon: null,
  
              dropProcessing: 'qq-drop-processing',
              dropProcessingSpinner: 'qq-drop-processing-spinner'
          },
          failedUploadTextDisplay: {
              mode: 'default', //default, custom, or none
              maxChars: 50,
              responseProperty: 'error',
              enableTooltip: true
          },
          messages: {
              tooManyFilesError: "You may only drop one file"
          },
          retry: {
              showAutoRetryNote: true,
              autoRetryNote: "Retrying {retryNum}/{maxAuto}...",
              showButton: false
          },
          deleteFile: {
              forceConfirm: false,
              confirmMessage: "Are you sure you want to delete {filename}?",
              deletingStatusText: "Deleting...",
              deletingFailedText: "Delete failed"
  
          },
          display: {
              fileSizeOnSubmit: false
          },
          paste: {
              promptForName: false,
              namePromptMessage: "Please name this image"
          },
          showMessage: function(message){
              setTimeout(function() {
                  window.alert(message);
              }, 0);
          },
          showConfirm: function(message, okCallback, cancelCallback) {
              setTimeout(function() {
                  var result = window.confirm(message);
                  if (result) {
                      okCallback();
                  }
                  else if (cancelCallback) {
                      cancelCallback();
                  }
              }, 0);
          },
          showPrompt: function(message, defaultValue) {
              var promise = new qq.Promise(),
                  retVal = window.prompt(message, defaultValue);
  
              /*jshint eqeqeq: true, eqnull: true*/
              if (retVal != null && qq.trimStr(retVal).length > 0) {
                  promise.success(retVal);
              }
              else {
                  promise.failure("Undefined or invalid user-supplied value.");
              }
  
              return promise;
          }
      }, true);
  
      // overwrite options with user supplied
      qq.extend(this._options, o, true);
      this._wrapCallbacks();
  
      // overwrite the upload button text if any
      // same for the Cancel button and Fail message text
      this._options.template     = this._options.template.replace(/\{dragZoneText\}/g, this._options.text.dragZone);
      this._options.template     = this._options.template.replace(/\{uploadButtonText\}/g, this._options.text.uploadButton);
      this._options.template     = this._options.template.replace(/\{dropProcessingText\}/g, this._options.text.dropProcessing);
      this._options.fileTemplate = this._options.fileTemplate.replace(/\{cancelButtonText\}/g, this._options.text.cancelButton);
      this._options.fileTemplate = this._options.fileTemplate.replace(/\{retryButtonText\}/g, this._options.text.retryButton);
      this._options.fileTemplate = this._options.fileTemplate.replace(/\{deleteButtonText\}/g, this._options.text.deleteButton);
      this._options.fileTemplate = this._options.fileTemplate.replace(/\{statusText\}/g, "");
  
      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;
  
      if (!this._button) {
          this._button = this._createUploadButton(this._find(this._element, 'button'));
      }
  
      this._bindCancelAndRetryEvents();
  
      this._dnd = this._setupDragAndDrop();
  
      if (this._options.paste.targetElement && this._options.paste.promptForName) {
          this._setupPastePrompt();
      }
  };
  
  // inherit from Basic Uploader
  qq.extend(qq.FineUploader.prototype, qq.FineUploaderBasic.prototype);
  
  qq.extend(qq.FineUploader.prototype, {
      clearStoredFiles: function() {
          qq.FineUploaderBasic.prototype.clearStoredFiles.apply(this, arguments);
          this._listElement.innerHTML = "";
      },
      addExtraDropzone: function(element){
          this._dnd.setupExtraDropzone(element);
      },
      removeExtraDropzone: function(element){
          return this._dnd.removeExtraDropzone(element);
      },
      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;
          }
      },
      reset: function() {
          qq.FineUploaderBasic.prototype.reset.apply(this, arguments);
          this._element.innerHTML = this._options.template;
          this._listElement = this._options.listElement || this._find(this._element, 'list');
          if (!this._options.button) {
              this._button = this._createUploadButton(this._find(this._element, 'button'));
          }
          this._bindCancelAndRetryEvents();
          this._dnd.dispose();
          this._dnd = this._setupDragAndDrop();
      },
      _removeFileItem: function(fileId) {
          var item = this.getItemByFileId(fileId);
          qq(item).remove();
      },
      _setupDragAndDrop: function() {
          var self = this,
              dropProcessingEl = this._find(this._element, 'dropProcessing'),
              dnd, preventSelectFiles, defaultDropAreaEl;
  
          preventSelectFiles = function(event) {
              event.preventDefault();
          };
  
          if (!this._options.dragAndDrop.disableDefaultDropzone) {
              defaultDropAreaEl = this._find(this._options.element, 'drop');
          }
  
          dnd = new qq.DragAndDrop({
              dropArea: defaultDropAreaEl,
              extraDropzones: this._options.dragAndDrop.extraDropzones,
              hideDropzones: this._options.dragAndDrop.hideDropzones,
              multiple: this._options.multiple,
              classes: {
                  dropActive: this._options.classes.dropActive
              },
              callbacks: {
                  dropProcessing: function(isProcessing, files) {
                      var input = self._button.getInput();
  
                      if (isProcessing) {
                          qq(dropProcessingEl).css({display: 'block'});
                          qq(input).attach('click', preventSelectFiles);
                      }
                      else {
                          qq(dropProcessingEl).hide();
                          qq(input).detach('click', preventSelectFiles);
                      }
  
                      if (files) {
                          self.addFiles(files);
                      }
                  },
                  error: function(code, filename) {
                      self._itemError(code, filename);
                  },
                  log: function(message, level) {
                      self.log(message, level);
                  }
              }
          });
  
          dnd.setup();
  
          return dnd;
      },
      _leaving_document_out: function(e){
          return ((qq.chrome() || (qq.safari() && qq.windows())) && e.clientX == 0 && e.clientY == 0) // null coords for Chrome and Safari Windows
              || (qq.firefox() && !e.relatedTarget); // null e.relatedTarget for Firefox
      },
      _storeForLater: function(id) {
          qq.FineUploaderBasic.prototype._storeForLater.apply(this, arguments);
          var item = this.getItemByFileId(id);
          qq(this._find(item, 'spinner')).hide();
      },
      /**
       * Gets one of the elements listed in this._options.classes
       **/
      _find: function(parent, type){
          var element = qq(parent).getByClass(this._options.classes[type])[0];
          if (!element){
              throw new Error('element not found ' + type);
          }
  
          return element;
      },
      _onSubmit: function(id, name){
          qq.FineUploaderBasic.prototype._onSubmit.apply(this, arguments);
          this._addToList(id, name);
      },
      // Update the progress bar & percentage as the file is uploaded
      _onProgress: function(id, name, loaded, total){
          qq.FineUploaderBasic.prototype._onProgress.apply(this, arguments);
  
          var item, progressBar, percent, cancelLink;
  
          item = this.getItemByFileId(id);
          progressBar = this._find(item, 'progressBar');
          percent = Math.round(loaded / total * 100);
  
          if (loaded === total) {
              cancelLink = this._find(item, 'cancel');
              qq(cancelLink).hide();
  
              qq(progressBar).hide();
              qq(this._find(item, 'statusText')).setText(this._options.text.waitingForResponse);
  
              // If last byte was sent, display total file size
              this._displayFileSize(id);
          }
          else {
              // If still uploading, display percentage - total size is actually the total request(s) size
              this._displayFileSize(id, loaded, total);
  
              qq(progressBar).css({display: 'block'});
          }
  
          // Update progress bar element
          qq(progressBar).css({width: percent + '%'});
      },
      _onComplete: function(id, name, result, xhr){
          qq.FineUploaderBasic.prototype._onComplete.apply(this, arguments);
  
          var item = this.getItemByFileId(id);
  
          qq(this._find(item, 'statusText')).clearText();
  
          qq(item).removeClass(this._classes.retrying);
          qq(this._find(item, 'progressBar')).hide();
  
          if (!this._options.disableCancelForFormUploads || qq.isXhrUploadSupported()) {
              qq(this._find(item, 'cancel')).hide();
          }
          qq(this._find(item, 'spinner')).hide();
  
          if (result.success) {
              if (this._isDeletePossible()) {
                  this._showDeleteLink(id);
              }
  
              qq(item).addClass(this._classes.success);
              if (this._classes.successIcon) {
                  this._find(item, 'finished').style.display = "inline-block";
                  qq(item).addClass(this._classes.successIcon);
              }
          } else {
              qq(item).addClass(this._classes.fail);
              if (this._classes.failIcon) {
                  this._find(item, 'finished').style.display = "inline-block";
                  qq(item).addClass(this._classes.failIcon);
              }
              if (this._options.retry.showButton && !this._preventRetries[id]) {
                  qq(item).addClass(this._classes.retryable);
              }
              this._controlFailureTextDisplay(item, result);
          }
      },
      _onUpload: function(id, name){
          qq.FineUploaderBasic.prototype._onUpload.apply(this, arguments);
  
          this._showSpinner(id);
      },
      _onCancel: function(id, name) {
          qq.FineUploaderBasic.prototype._onCancel.apply(this, arguments);
          this._removeFileItem(id);
      },
      _onBeforeAutoRetry: function(id) {
          var item, progressBar, failTextEl, retryNumForDisplay, maxAuto, retryNote;
  
          qq.FineUploaderBasic.prototype._onBeforeAutoRetry.apply(this, arguments);
  
          item = this.getItemByFileId(id);
          progressBar = this._find(item, 'progressBar');
  
          this._showCancelLink(item);
          progressBar.style.width = 0;
          qq(progressBar).hide();
  
          if (this._options.retry.showAutoRetryNote) {
              failTextEl = this._find(item, 'statusText');
              retryNumForDisplay = this._autoRetries[id] + 1;
              maxAuto = this._options.retry.maxAutoAttempts;
  
              retryNote = this._options.retry.autoRetryNote.replace(/\{retryNum\}/g, retryNumForDisplay);
              retryNote = retryNote.replace(/\{maxAuto\}/g, maxAuto);
  
              qq(failTextEl).setText(retryNote);
              if (retryNumForDisplay === 1) {
                  qq(item).addClass(this._classes.retrying);
              }
          }
      },
      //return false if we should not attempt the requested retry
      _onBeforeManualRetry: function(id) {
          var item = this.getItemByFileId(id);
  
          if (qq.FineUploaderBasic.prototype._onBeforeManualRetry.apply(this, arguments)) {
              this._find(item, 'progressBar').style.width = 0;
              qq(item).removeClass(this._classes.fail);
              qq(this._find(item, 'statusText')).clearText();
              this._showSpinner(id);
              this._showCancelLink(item);
              return true;
          }
          else {
              qq(item).addClass(this._classes.retryable);
              return false;
          }
      },
      _onSubmitDelete: function(id) {
          if (this._isDeletePossible()) {
              if (this._options.callbacks.onSubmitDelete(id) !== false) {
                  if (this._options.deleteFile.forceConfirm) {
                      this._showDeleteConfirm(id);
                  }
                  else {
                      this._sendDeleteRequest(id);
                  }
              }
          }
          else {
              this.log("Delete request ignored for file ID " + id + ", delete feature is disabled.", "warn");
              return false;
          }
      },
      _onDeleteComplete: function(id, xhr, isError) {
          qq.FineUploaderBasic.prototype._onDeleteComplete.apply(this, arguments);
  
          var item = this.getItemByFileId(id),
              spinnerEl = this._find(item, 'spinner'),
              statusTextEl = this._find(item, 'statusText');
  
          qq(spinnerEl).hide();
  
          if (isError) {
              qq(statusTextEl).setText(this._options.deleteFile.deletingFailedText);
              this._showDeleteLink(id);
          }
          else {
              this._removeFileItem(id);
          }
      },
      _sendDeleteRequest: function(id) {
          var item = this.getItemByFileId(id),
              deleteLink = this._find(item, 'deleteButton'),
              statusTextEl = this._find(item, 'statusText');
  
          qq(deleteLink).hide();
          this._showSpinner(id);
          qq(statusTextEl).setText(this._options.deleteFile.deletingStatusText);
          this._deleteHandler.sendDelete(id, this.getUuid(id));
      },
      _showDeleteConfirm: function(id) {
          var fileName = this._handler.getName(id),
              confirmMessage = this._options.deleteFile.confirmMessage.replace(/\{filename\}/g, fileName),
              uuid = this.getUuid(id),
              self = this;
  
          this._options.showConfirm(confirmMessage, function() {
              self._sendDeleteRequest(id);
          });
      },
      _addToList: function(id, name){
          var item = qq.toElement(this._options.fileTemplate);
          if (this._options.disableCancelForFormUploads && !qq.isXhrUploadSupported()) {
              var cancelLink = this._find(item, 'cancel');
              qq(cancelLink).remove();
          }
  
          item.qqFileId = id;
  
          var fileElement = this._find(item, 'file');
          qq(fileElement).setText(this._options.formatFileName(name));
          qq(this._find(item, 'size')).hide();
          if (!this._options.multiple) {
              this._handler.cancelAll();
              this._clearList();
          }
  
          this._listElement.appendChild(item);
  
          if (this._options.display.fileSizeOnSubmit && qq.isXhrUploadSupported()) {
              this._displayFileSize(id);
          }
      },
      _clearList: function(){
          this._listElement.innerHTML = '';
          this.clearStoredFiles();
      },
      _displayFileSize: function(id, loadedSize, totalSize) {
          var item = this.getItemByFileId(id),
              size = this.getSize(id),
              sizeForDisplay = this._formatSize(size),
              sizeEl = this._find(item, 'size');
  
          if (loadedSize !== undefined && totalSize !== undefined) {
              sizeForDisplay = this._formatProgress(loadedSize, totalSize);
          }
  
          qq(sizeEl).css({display: 'inline'});
          qq(sizeEl).setText(sizeForDisplay);
      },
      /**
       * delegate click event for cancel & retry links
       **/
      _bindCancelAndRetryEvents: function(){
          var self = this,
              list = this._listElement;
  
          this._disposeSupport.attach(list, 'click', function(e){
              e = e || window.event;
              var target = e.target || e.srcElement;
  
              if (qq(target).hasClass(self._classes.cancel) || qq(target).hasClass(self._classes.retry) || qq(target).hasClass(self._classes.deleteButton)){
                  qq.preventDefault(e);
  
                  var item = target.parentNode;
                  while(item.qqFileId === undefined) {
                      item = target = target.parentNode;
                  }
  
                  if (qq(target).hasClass(self._classes.deleteButton)) {
                      self.deleteFile(item.qqFileId);
                  }
                  else if (qq(target).hasClass(self._classes.cancel)) {
                      self.cancel(item.qqFileId);
                  }
                  else {
                      qq(item).removeClass(self._classes.retryable);
                      self.retry(item.qqFileId);
                  }
              }
          });
      },
      _formatProgress: function (uploadedSize, totalSize) {
          var message = this._options.text.formatProgress;
          function r(name, replacement) { message = message.replace(name, replacement); }
  
          r('{percent}', Math.round(uploadedSize / totalSize * 100));
          r('{total_size}', this._formatSize(totalSize));
          return message;
      },
      _controlFailureTextDisplay: function(item, response) {
          var mode, maxChars, responseProperty, failureReason, shortFailureReason;
  
          mode = this._options.failedUploadTextDisplay.mode;
          maxChars = this._options.failedUploadTextDisplay.maxChars;
          responseProperty = this._options.failedUploadTextDisplay.responseProperty;
  
          if (mode === 'custom') {
              failureReason = response[responseProperty];
              if (failureReason) {
                  if (failureReason.length > maxChars) {
                      shortFailureReason = failureReason.substring(0, maxChars) + '...';
                  }
              }
              else {
                  failureReason = this._options.text.failUpload;
                  this.log("'" + responseProperty + "' is not a valid property on the server response.", 'warn');
              }
  
              qq(this._find(item, 'statusText')).setText(shortFailureReason || failureReason);
  
              if (this._options.failedUploadTextDisplay.enableTooltip) {
                  this._showTooltip(item, failureReason);
              }
          }
          else if (mode === 'default') {
              qq(this._find(item, 'statusText')).setText(this._options.text.failUpload);
          }
          else if (mode !== 'none') {
              this.log("failedUploadTextDisplay.mode value of '" + mode + "' is not valid", 'warn');
          }
      },
      _showTooltip: function(item, text) {
          item.title = text;
      },
      _showSpinner: function(id) {
          var item = this.getItemByFileId(id),
              spinnerEl = this._find(item, 'spinner');
  
          spinnerEl.style.display = "inline-block";
      },
      _showCancelLink: function(item) {
          if (!this._options.disableCancelForFormUploads || qq.isXhrUploadSupported()) {
              var cancelLink = this._find(item, 'cancel');
  
              qq(cancelLink).css({display: 'inline'});
          }
      },
      _showDeleteLink: function(id) {
          var item = this.getItemByFileId(id),
              deleteLink = this._find(item, 'deleteButton');
  
          qq(deleteLink).css({display: 'inline'});
      },
      _itemError: function(code, name){
          var message = qq.FineUploaderBasic.prototype._itemError.apply(this, arguments);
          this._options.showMessage(message);
      },
      _batchError: function(message) {
          qq.FineUploaderBasic.prototype._batchError.apply(this, arguments);
          this._options.showMessage(message);
      },
      _setupPastePrompt: function() {
          var self = this;
  
          this._options.callbacks.onPasteReceived = function() {
              var message = self._options.paste.namePromptMessage,
                  defaultVal = self._options.paste.defaultName;
  
              return self._options.showPrompt(message, defaultVal);
          };
      }
  });
  /** Generic class for sending non-upload ajax requests and handling the associated responses **/
  //TODO Use XDomainRequest if expectCors = true.  Not necessary now since only DELETE requests are sent and XDR doesn't support pre-flighting.
  /*globals qq, XMLHttpRequest*/
  qq.AjaxRequestor = function(o) {
      "use strict";
  
      var log, shouldParamsBeInQueryString,
          queue = [],
          requestState = [],
          options = {
              method: 'POST',
              maxConnections: 3,
              customHeaders: {},
              endpointStore: {},
              paramsStore: {},
              successfulResponseCodes: [200],
              demoMode: false,
              cors: {
                  expected: false,
                  sendCredentials: false
              },
              log: function(str, level) {},
              onSend: function(id) {},
              onComplete: function(id, xhr, isError) {},
              onCancel: function(id) {}
          };
  
      qq.extend(options, o);
      log = options.log;
      shouldParamsBeInQueryString = getMethod() === 'GET' || getMethod() === 'DELETE';
  
  
      /**
       * Removes element from queue, sends next request
       */
      function dequeue(id) {
          var i = qq.indexOf(queue, id),
              max = options.maxConnections,
              nextId;
  
          delete requestState[id];
          queue.splice(i, 1);
  
          if (queue.length >= max && i < max){
              nextId = queue[max-1];
              sendRequest(nextId);
          }
      }
  
      function onComplete(id) {
          var xhr = requestState[id].xhr,
              method = getMethod(),
              isError = false;
  
          dequeue(id);
  
          if (!isResponseSuccessful(xhr.status)) {
              isError = true;
              log(method + " request for " + id + " has failed - response code " + xhr.status, "error");
          }
  
          options.onComplete(id, xhr, isError);
      }
  
      function sendRequest(id) {
          var xhr = new XMLHttpRequest(),
              method = getMethod(),
              params = {},
              url;
  
          options.onSend(id);
  
          if (options.paramsStore.getParams) {
              params = options.paramsStore.getParams(id);
          }
  
          url = createUrl(id, params);
  
          requestState[id].xhr = xhr;
          xhr.onreadystatechange = getReadyStateChangeHandler(id);
          xhr.open(method, url, true);
  
          if (options.cors.expected && options.cors.sendCredentials) {
              xhr.withCredentials = true;
          }
  
          setHeaders(id);
  
          log('Sending ' + method + " request for " + id);
          if (!shouldParamsBeInQueryString && params) {
              xhr.send(qq.obj2url(params, ""));
          }
          else {
              xhr.send();
          }
      }
  
      function createUrl(id, params) {
          var endpoint = options.endpointStore.getEndpoint(id),
              addToPath = requestState[id].addToPath;
  
          if (addToPath !== undefined) {
              endpoint += "/" + addToPath;
          }
  
          if (shouldParamsBeInQueryString && params) {
              return qq.obj2url(params, endpoint);
          }
          else {
              return endpoint;
          }
      }
  
      function getReadyStateChangeHandler(id) {
          var xhr = requestState[id].xhr;
  
          return function() {
              if (xhr.readyState === 4) {
                  onComplete(id, xhr);
              }
          };
      }
  
      function setHeaders(id) {
          var xhr = requestState[id].xhr,
              customHeaders = options.customHeaders;
  
          xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
          xhr.setRequestHeader("Cache-Control", "no-cache");
  
          qq.each(customHeaders, function(name, val) {
              xhr.setRequestHeader(name, val);
          });
      }
  
      function cancelRequest(id) {
          var xhr = requestState[id].xhr,
              method = getMethod();
  
          if (xhr) {
              xhr.onreadystatechange = null;
              xhr.abort();
              dequeue(id);
  
              log('Cancelled ' + method + " for " + id);
              options.onCancel(id);
  
              return true;
          }
  
          return false;
      }
  
      function isResponseSuccessful(responseCode) {
          return qq.indexOf(options.successfulResponseCodes, responseCode) >= 0;
      }
  
      function getMethod() {
          if (options.demoMode) {
              return "GET";
          }
  
          return options.method;
      }
  
  
      return {
          send: function(id, addToPath) {
              requestState[id] = {
                  addToPath: addToPath
              };
  
              var len = queue.push(id);
  
              // if too many active connections, wait...
              if (len <= options.maxConnections){
                  sendRequest(id);
              }
          },
          cancel: function(id) {
              return cancelRequest(id);
          }
      };
  };
  /** Generic class for sending non-upload ajax requests and handling the associated responses **/
  /*globals qq, XMLHttpRequest*/
  qq.DeleteFileAjaxRequestor = function(o) {
      "use strict";
  
      var requestor,
          options = {
              endpointStore: {},
              maxConnections: 3,
              customHeaders: {},
              paramsStore: {},
              demoMode: false,
              cors: {
                  expected: false,
                  sendCredentials: false
              },
              log: function(str, level) {},
              onDelete: function(id) {},
              onDeleteComplete: function(id, xhr, isError) {}
          };
  
      qq.extend(options, o);
  
      requestor = new qq.AjaxRequestor({
          method: 'DELETE',
          endpointStore: options.endpointStore,
          paramsStore: options.paramsStore,
          maxConnections: options.maxConnections,
          customHeaders: options.customHeaders,
          successfulResponseCodes: [200, 202, 204],
          demoMode: options.demoMode,
          log: options.log,
          onSend: options.onDelete,
          onComplete: options.onDeleteComplete
      });
  
  
      return {
          sendDelete: function(id, uuid) {
              requestor.send(id, uuid);
              options.log("Submitted delete file request for " + id);
          }
      };
  };
  qq.WindowReceiveMessage = function(o) {
      var options = {
              log: function(message, level) {}
          },
          callbackWrapperDetachers = {};
  
      qq.extend(options, o);
  
      return {
          receiveMessage : function(id, callback) {
              var onMessageCallbackWrapper = function(event) {
                      callback(event.data);
                  };
  
              if (window.postMessage) {
                  callbackWrapperDetachers[id] = qq(window).attach("message", onMessageCallbackWrapper);
              }
              else {
                  log("iframe message passing not supported in this browser!", "error");
              }
          },
  
          stopReceivingMessages : function(id) {
              if (window.postMessage) {
                  var detacher = callbackWrapperDetachers[id];
                  if (detacher) {
                      detacher();
                  }
              }
          }
      };
  };
  /**
   * Class for uploading files, uploading itself is handled by child classes
   */
  /*globals qq*/
  qq.UploadHandler = function(o) {
      "use strict";
  
      var queue = [],
          options, log, dequeue, handlerImpl;
  
      // Default options, can be overridden by the user
      options = {
          debug: false,
          forceMultipart: true,
          paramsInBody: false,
          paramsStore: {},
          endpointStore: {},
          cors: {
              expected: false,
              sendCredentials: false
          },
          maxConnections: 3, // maximum number of concurrent uploads
          uuidParamName: 'qquuid',
          totalFileSizeParamName: 'qqtotalfilesize',
          chunking: {
              enabled: false,
              partSize: 2000000, //bytes
              paramNames: {
                  partIndex: 'qqpartindex',
                  partByteOffset: 'qqpartbyteoffset',
                  chunkSize: 'qqchunksize',
                  totalParts: 'qqtotalparts',
                  filename: 'qqfilename'
              }
          },
          resume: {
              enabled: false,
              id: null,
              cookiesExpireIn: 7, //days
              paramNames: {
                  resuming: "qqresume"
              }
          },
          blobs: {
              paramNames: {
                  name: 'qqblobname'
              }
          },
          log: function(str, level) {},
          onProgress: function(id, fileName, loaded, total){},
          onComplete: function(id, fileName, response, xhr){},
          onCancel: function(id, fileName){},
          onUpload: function(id, fileName){},
          onUploadChunk: function(id, fileName, chunkData){},
          onAutoRetry: function(id, fileName, response, xhr){},
          onResume: function(id, fileName, chunkData){}
  
      };
      qq.extend(options, o);
  
      log = options.log;
  
      /**
       * Removes element from queue, starts upload of next
       */
      dequeue = function(id) {
          var i = qq.indexOf(queue, id),
              max = options.maxConnections,
              nextId;
  
          if (i >= 0) {
              queue.splice(i, 1);
  
              if (queue.length >= max && i < max){
                  nextId = queue[max-1];
                  handlerImpl.upload(nextId);
              }
          }
      };
  
      if (qq.isXhrUploadSupported()) {
          handlerImpl = new qq.UploadHandlerXhr(options, dequeue, log);
      }
      else {
          handlerImpl = new qq.UploadHandlerForm(options, dequeue, log);
      }
  
  
      return {
          /**
           * Adds file or file input to the queue
           * @returns id
           **/
          add: function(file){
              return handlerImpl.add(file);
          },
          /**
           * Sends the file identified by id
           */
          upload: function(id){
              var len = queue.push(id);
  
              // if too many active uploads, wait...
              if (len <= options.maxConnections){
                  return handlerImpl.upload(id);
              }
          },
          retry: function(id) {
              var i = qq.indexOf(queue, id);
              if (i >= 0) {
                  return handlerImpl.upload(id, true);
              }
              else {
                  return this.upload(id);
              }
          },
          /**
           * Cancels file upload by id
           */
          cancel: function(id) {
              log('Cancelling ' + id);
              options.paramsStore.remove(id);
              handlerImpl.cancel(id);
              dequeue(id);
          },
          /**
           * Cancels all queued or in-progress uploads
           */
          cancelAll: function() {
              var self = this,
                  queueCopy = [];
  
              qq.extend(queueCopy, queue);
              qq.each(queueCopy, function(idx, fileId) {
                  self.cancel(fileId);
              });
  
              queue = [];
          },
          /**
           * Returns name of the file identified by id
           */
          getName: function(id){
              return handlerImpl.getName(id);
          },
          /**
           * Returns size of the file identified by id
           */
          getSize: function(id){
              if (handlerImpl.getSize) {
                  return handlerImpl.getSize(id);
              }
          },
          getFile: function(id) {
              if (handlerImpl.getFile) {
                  return handlerImpl.getFile(id);
              }
          },
          /**
           * Returns id of files being uploaded or
           * waiting for their turn
           */
          getQueue: function(){
              return queue;
          },
          reset: function() {
              log('Resetting upload handler');
              queue = [];
              handlerImpl.reset();
          },
          getUuid: function(id) {
              return handlerImpl.getUuid(id);
          },
          /**
           * Determine if the file exists.
           */
          isValid: function(id) {
              return handlerImpl.isValid(id);
          },
          getResumableFilesData: function() {
              if (handlerImpl.getResumableFilesData) {
                  return handlerImpl.getResumableFilesData();
              }
              return [];
          }
      };
  };
  /*globals qq, document, setTimeout*/
  /*globals clearTimeout*/
  qq.UploadHandlerForm = function(o, uploadCompleteCallback, logCallback) {
      "use strict";
  
      var options = o,
          inputs = [],
          uuids = [],
          detachLoadEvents = {},
          postMessageCallbackTimers = {},
          uploadComplete = uploadCompleteCallback,
          log = logCallback,
          corsMessageReceiver = new qq.WindowReceiveMessage({log: log}),
          onloadCallbacks = {},
          api;
  
  
      function detachLoadEvent(id) {
          if (detachLoadEvents[id] !== undefined) {
              detachLoadEvents[id]();
              delete detachLoadEvents[id];
          }
      }
  
      function registerPostMessageCallback(iframe, callback) {
          var id = iframe.id;
  
          onloadCallbacks[uuids[id]] = callback;
  
          detachLoadEvents[id] = qq(iframe).attach('load', function() {
              if (inputs[id]) {
                  log("Received iframe load event for CORS upload request (file id " + id + ")");
  
                  postMessageCallbackTimers[id] = setTimeout(function() {
                      var errorMessage = "No valid message received from loaded iframe for file id " + id;
                      log(errorMessage, "error");
                      callback({
                          error: errorMessage
                      });
                  }, 1000);
              }
          });
  
          corsMessageReceiver.receiveMessage(id, function(message) {
              log("Received the following window message: '" + message + "'");
              var response = qq.parseJson(message),
                  uuid = response.uuid,
                  onloadCallback;
  
              if (uuid && onloadCallbacks[uuid]) {
                  clearTimeout(postMessageCallbackTimers[id]);
                  delete postMessageCallbackTimers[id];
  
                  detachLoadEvent(id);
  
                  onloadCallback = onloadCallbacks[uuid];
  
                  delete onloadCallbacks[uuid];
                  corsMessageReceiver.stopReceivingMessages(id);
                  onloadCallback(response);
              }
              else if (!uuid) {
                  log("'" + message + "' does not contain a UUID - ignoring.");
              }
          });
      }
  
      function attachLoadEvent(iframe, callback) {
          /*jslint eqeq: true*/
  
          if (options.cors.expected) {
              registerPostMessageCallback(iframe, callback);
          }
          else {
              detachLoadEvents[iframe.id] = qq(iframe).attach('load', function(){
                  log('Received response for ' + iframe.id);
  
                  // when we remove iframe from dom
                  // the request stops, but in IE load
                  // event fires
                  if (!iframe.parentNode){
                      return;
                  }
  
                  try {
                      // 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;
                      }
                  }
                  catch (error) {
                      //IE may throw an "access is denied" error when attempting to access contentDocument on the iframe in some cases
                      log('Error when attempting to access iframe during handling of upload response (' + error + ")", 'error');
                  }
  
                  callback();
              });
          }
      }
  
      /**
       * Returns json object received by iframe from server.
       */
      function getIframeContentJson(iframe) {
          /*jshint evil: true*/
  
          var response;
  
          //IE may throw an "access is denied" error when attempting to access contentDocument on the iframe in some cases
          try {
              // iframe.contentWindow.document - for IE<7
              var doc = iframe.contentDocument || iframe.contentWindow.document,
                  innerHTML = doc.body.innerHTML;
  
              log("converting iframe's innerHTML to JSON");
              log("innerHTML = " + innerHTML);
              //plain text response may be wrapped in <pre> tag
              if (innerHTML && innerHTML.match(/^<pre/i)) {
                  innerHTML = doc.body.firstChild.firstChild.nodeValue;
              }
  
              response = qq.parseJson(innerHTML);
          } catch(error){
              log('Error when attempting to parse form upload response (' + error + ")", 'error');
              response = {success: false};
          }
  
          return response;
      }
  
      /**
       * Creates iframe with unique name
       */
      function createIframe(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 + '" />');
  
          iframe.setAttribute('id', id);
  
          iframe.style.display = 'none';
          document.body.appendChild(iframe);
  
          return iframe;
      }
  
      /**
       * Creates form, that will be submitted to iframe
       */
      function createForm(id, iframe){
          var params = options.paramsStore.getParams(id),
              protocol = options.demoMode ? "GET" : "POST",
              form = qq.toElement('<form method="' + protocol + '" enctype="multipart/form-data"></form>'),
              endpoint = options.endpointStore.getEndpoint(id),
              url = endpoint;
  
          params[options.uuidParamName] = uuids[id];
  
          if (!options.paramsInBody) {
              url = qq.obj2url(params, endpoint);
          }
          else {
              qq.obj2Inputs(params, form);
          }
  
          form.setAttribute('action', url);
          form.setAttribute('target', iframe.name);
          form.style.display = 'none';
          document.body.appendChild(form);
  
          return form;
      }
  
  
      api = {
          add: function(fileInput) {
              fileInput.setAttribute('name', options.inputName);
  
              var id = inputs.push(fileInput) - 1;
              uuids[id] = qq.getUniqueId();
  
              // remove file input from DOM
              if (fileInput.parentNode){
                  qq(fileInput).remove();
              }
  
              return id;
          },
          getName: function(id) {
              /*jslint regexp: true*/
  
              if (api.isValid(id)) {
                  // get input value and remove path to normalize
                  return inputs[id].value.replace(/.*(\/|\\)/, "");
              }
              else {
                  log(id + " is not a valid item ID.", "error");
              }
          },
          isValid: function(id) {
              return inputs[id] !== undefined;
          },
          reset: function() {
              inputs = [];
              uuids = [];
              detachLoadEvents = {};
          },
          getUuid: function(id) {
              return uuids[id];
          },
          cancel: function(id) {
              options.onCancel(id, this.getName(id));
  
              delete inputs[id];
              delete uuids[id];
              delete detachLoadEvents[id];
  
              if (options.cors.expected) {
                  clearTimeout(postMessageCallbackTimers[id]);
                  delete postMessageCallbackTimers[id];
                  corsMessageReceiver.stopReceivingMessages(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', 'java' + String.fromCharCode(115) + 'cript:false;'); //deal with "JSLint: javascript URL" warning, which apparently cannot be turned off
  
                  qq(iframe).remove();
              }
          },
          upload: function(id){
              var input = inputs[id],
                  fileName = api.getName(id),
                  iframe = createIframe(id),
                  form;
  
              if (!input){
                  throw new Error('file with passed id was not added, or already uploaded or cancelled');
              }
  
              options.onUpload(id, this.getName(id));
  
              form = createForm(id, iframe);
              form.appendChild(input);
  
              attachLoadEvent(iframe, function(responseFromMessage){
                  log('iframe loaded');
  
                  var response = responseFromMessage ? responseFromMessage : getIframeContentJson(iframe);
  
                  detachLoadEvent(id);
  
                  //we can't remove an iframe if the iframe doesn't belong to the same domain
                  if (!options.cors.expected) {
                      qq(iframe).remove();
                  }
  
                  if (!response.success) {
                      if (options.onAutoRetry(id, fileName, response)) {
                          return;
                      }
                  }
                  options.onComplete(id, fileName, response);
                  uploadComplete(id);
              });
  
              log('Sending upload request for ' + id);
              form.submit();
              qq(form).remove();
  
              return id;
          }
      };
  
      return api;
  };
  /*globals qq, File, XMLHttpRequest, FormData, Blob*/
  qq.UploadHandlerXhr = function(o, uploadCompleteCallback, logCallback) {
      "use strict";
      
      var options = o,
          uploadComplete = uploadCompleteCallback,
          log = logCallback,
          fileState = [],
          cookieItemDelimiter = "|",
          chunkFiles = options.chunking.enabled && qq.isFileChunkingSupported(),
          resumeEnabled = options.resume.enabled && chunkFiles && qq.areCookiesEnabled(),
          resumeId = getResumeId(),
          multipart = options.forceMultipart || options.paramsInBody,
          api;
  
  
       function addChunkingSpecificParams(id, params, chunkData) {
          var size = api.getSize(id),
              name = api.getName(id);
  
          params[options.chunking.paramNames.partIndex] = chunkData.part;
          params[options.chunking.paramNames.partByteOffset] = chunkData.start;
          params[options.chunking.paramNames.chunkSize] = chunkData.size;
          params[options.chunking.paramNames.totalParts] = chunkData.count;
          params[options.totalFileSizeParamName] = size;
  
          /**
           * When a Blob is sent in a multipart request, the filename value in the content-disposition header is either "blob"
           * or an empty string.  So, we will need to include the actual file name as a param in this case.
           */
          if (multipart) {
              params[options.chunking.paramNames.filename] = name;
          }
      }
  
      function addResumeSpecificParams(params) {
          params[options.resume.paramNames.resuming] = true;
      }
  
       function getChunk(fileOrBlob, startByte, endByte) {
          if (fileOrBlob.slice) {
              return fileOrBlob.slice(startByte, endByte);
          }
          else if (fileOrBlob.mozSlice) {
              return fileOrBlob.mozSlice(startByte, endByte);
          }
          else if (fileOrBlob.webkitSlice) {
              return fileOrBlob.webkitSlice(startByte, endByte);
          }
      }
  
      function getChunkData(id, chunkIndex) {
          var chunkSize = options.chunking.partSize,
              fileSize = api.getSize(id),
              fileOrBlob = fileState[id].file || fileState[id].blobData.blob,
              startBytes = chunkSize * chunkIndex,
              endBytes = startBytes+chunkSize >= fileSize ? fileSize : startBytes+chunkSize,
              totalChunks = getTotalChunks(id);
  
          return {
              part: chunkIndex,
              start: startBytes,
              end: endBytes,
              count: totalChunks,
              blob: getChunk(fileOrBlob, startBytes, endBytes),
              size: endBytes - startBytes
          };
      }
  
      function getTotalChunks(id) {
          var fileSize = api.getSize(id),
              chunkSize = options.chunking.partSize;
  
          return Math.ceil(fileSize / chunkSize);
      }
  
      function createXhr(id) {
          var xhr = new XMLHttpRequest();
  
          fileState[id].xhr = xhr;
  
          return xhr;
      }
  
      function setParamsAndGetEntityToSend(params, xhr, fileOrBlob, id) {
          var formData = new FormData(),
              method = options.demoMode ? "GET" : "POST",
              endpoint = options.endpointStore.getEndpoint(id),
              url = endpoint,
              name = api.getName(id),
              size = api.getSize(id),
              blobData = fileState[id].blobData;
  
          params[options.uuidParamName] = fileState[id].uuid;
  
          if (multipart) {
              params[options.totalFileSizeParamName] = size;
  
              if (blobData) {
                  /**
                   * When a Blob is sent in a multipart request, the filename value in the content-disposition header is either "blob"
                   * or an empty string.  So, we will need to include the actual file name as a param in this case.
                   */
                  params[options.blobs.paramNames.name] = blobData.name;
              }
          }
  
          //build query string
          if (!options.paramsInBody) {
              if (!multipart) {
                  params[options.inputName] = name;
              }
              url = qq.obj2url(params, endpoint);
          }
  
          xhr.open(method, url, true);
  
          if (options.cors.expected && options.cors.sendCredentials) {
              xhr.withCredentials = true;
          }
  
          if (multipart) {
              if (options.paramsInBody) {
                  qq.obj2FormData(params, formData);
              }
  
              formData.append(options.inputName, fileOrBlob);
              return formData;
          }
  
          return fileOrBlob;
      }
  
      function setHeaders(id, xhr) {
          var extraHeaders = options.customHeaders,
              fileOrBlob = fileState[id].file || fileState[id].blobData.blob;
  
          xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
          xhr.setRequestHeader("Cache-Control", "no-cache");
  
          if (!multipart) {
              xhr.setRequestHeader("Content-Type", "application/octet-stream");
              //NOTE: return mime type in xhr works on chrome 16.0.9 firefox 11.0a2
              xhr.setRequestHeader("X-Mime-Type", fileOrBlob.type);
          }
  
          qq.each(extraHeaders, function(name, val) {
              xhr.setRequestHeader(name, val);
          });
      }
  
      function handleCompletedItem(id, response, xhr) {
          var name = api.getName(id),
              size = api.getSize(id);
  
          fileState[id].attemptingResume = false;
  
          options.onProgress(id, name, size, size);
  
          options.onComplete(id, name, response, xhr);
          delete fileState[id].xhr;
          uploadComplete(id);
      }
  
      function uploadNextChunk(id) {
          var chunkIdx = fileState[id].remainingChunkIdxs[0],
              chunkData = getChunkData(id, chunkIdx),
              xhr = createXhr(id),
              size = api.getSize(id),
              name = api.getName(id),
              toSend, params;
  
          if (fileState[id].loaded === undefined) {
              fileState[id].loaded = 0;
          }
  
          if (resumeEnabled && fileState[id].file) {
              persistChunkData(id, chunkData);
          }
  
          xhr.onreadystatechange = getReadyStateChangeHandler(id, xhr);
  
          xhr.upload.onprogress = function(e) {
              if (e.lengthComputable) {
                  var totalLoaded = e.loaded + fileState[id].loaded,
                      estTotalRequestsSize = calcAllRequestsSizeForChunkedUpload(id, chunkIdx, e.total);
  
                  options.onProgress(id, name, totalLoaded, estTotalRequestsSize);
              }
          };
  
          options.onUploadChunk(id, name, getChunkDataForCallback(chunkData));
  
          params = options.paramsStore.getParams(id);
          addChunkingSpecificParams(id, params, chunkData);
  
          if (fileState[id].attemptingResume) {
              addResumeSpecificParams(params);
          }
  
          toSend = setParamsAndGetEntityToSend(params, xhr, chunkData.blob, id);
          setHeaders(id, xhr);
  
          log('Sending chunked upload request for item ' + id + ": bytes " + (chunkData.start+1) + "-" + chunkData.end + " of " + size);
          xhr.send(toSend);
      }
  
      function calcAllRequestsSizeForChunkedUpload(id, chunkIdx, requestSize) {
          var chunkData = getChunkData(id, chunkIdx),
              blobSize = chunkData.size,
              overhead = requestSize - blobSize,
              size = api.getSize(id),
              chunkCount = chunkData.count,
              initialRequestOverhead = fileState[id].initialRequestOverhead,
              overheadDiff = overhead - initialRequestOverhead;
  
          fileState[id].lastRequestOverhead = overhead;
  
          if (chunkIdx === 0) {
              fileState[id].lastChunkIdxProgress = 0;
              fileState[id].initialRequestOverhead = overhead;
              fileState[id].estTotalRequestsSize = size + (chunkCount * overhead);
          }
          else if (fileState[id].lastChunkIdxProgress !== chunkIdx) {
              fileState[id].lastChunkIdxProgress = chunkIdx;
              fileState[id].estTotalRequestsSize += overheadDiff;
          }
  
          return fileState[id].estTotalRequestsSize;
      }
  
      function getLastRequestOverhead(id) {
          if (multipart) {
              return fileState[id].lastRequestOverhead;
          }
          else {
              return 0;
          }
      }
  
      function handleSuccessfullyCompletedChunk(id, response, xhr) {
          var chunkIdx = fileState[id].remainingChunkIdxs.shift(),
              chunkData = getChunkData(id, chunkIdx);
  
          fileState[id].attemptingResume = false;
          fileState[id].loaded += chunkData.size + getLastRequestOverhead(id);
  
          if (fileState[id].remainingChunkIdxs.length > 0) {
              uploadNextChunk(id);
          }
          else {
              if (resumeEnabled) {
                  deletePersistedChunkData(id);
              }
  
              handleCompletedItem(id, response, xhr);
          }
      }
  
      function isErrorResponse(xhr, response) {
          return xhr.status !== 200 || !response.success || response.reset;
      }
  
      function parseResponse(xhr) {
          var response;
  
          try {
              response = qq.parseJson(xhr.responseText);
          }
          catch(error) {
              log('Error when attempting to parse xhr response text (' + error + ')', 'error');
              response = {};
          }
  
          return response;
      }
  
      function handleResetResponse(id) {
          log('Server has ordered chunking effort to be restarted on next attempt for item ID ' + id, 'error');
  
          if (resumeEnabled) {
              deletePersistedChunkData(id);
              fileState[id].attemptingResume = false;
          }
  
          fileState[id].remainingChunkIdxs = [];
          delete fileState[id].loaded;
          delete fileState[id].estTotalRequestsSize;
          delete fileState[id].initialRequestOverhead;
      }
  
      function handleResetResponseOnResumeAttempt(id) {
          fileState[id].attemptingResume = false;
          log("Server has declared that it cannot handle resume for item ID " + id + " - starting from the first chunk", 'error');
          handleResetResponse(id);
          api.upload(id, true);
      }
  
      function handleNonResetErrorResponse(id, response, xhr) {
          var name = api.getName(id);
  
          if (options.onAutoRetry(id, name, response, xhr)) {
              return;
          }
          else {
              handleCompletedItem(id, response, xhr);
          }
      }
  
      function onComplete(id, xhr) {
          var response;
  
          // the request was aborted/cancelled
          if (!fileState[id]) {
              return;
          }
  
          log("xhr - server response received for " + id);
          log("responseText = " + xhr.responseText);
          response = parseResponse(xhr);
  
          if (isErrorResponse(xhr, response)) {
              if (response.reset) {
                  handleResetResponse(id);
              }
  
              if (fileState[id].attemptingResume && response.reset) {
                  handleResetResponseOnResumeAttempt(id);
              }
              else {
                  handleNonResetErrorResponse(id, response, xhr);
              }
          }
          else if (chunkFiles) {
              handleSuccessfullyCompletedChunk(id, response, xhr);
          }
          else {
              handleCompletedItem(id, response, xhr);
          }
      }
  
      function getChunkDataForCallback(chunkData) {
          return {
              partIndex: chunkData.part,
              startByte: chunkData.start + 1,
              endByte: chunkData.end,
              totalParts: chunkData.count
          };
      }
  
      function getReadyStateChangeHandler(id, xhr) {
          return function() {
              if (xhr.readyState === 4) {
                  onComplete(id, xhr);
              }
          };
      }
  
      function persistChunkData(id, chunkData) {
          var fileUuid = api.getUuid(id),
              lastByteSent = fileState[id].loaded,
              initialRequestOverhead = fileState[id].initialRequestOverhead,
              estTotalRequestsSize = fileState[id].estTotalRequestsSize,
              cookieName = getChunkDataCookieName(id),
              cookieValue = fileUuid +
                  cookieItemDelimiter + chunkData.part +
                  cookieItemDelimiter + lastByteSent +
                  cookieItemDelimiter + initialRequestOverhead +
                  cookieItemDelimiter + estTotalRequestsSize,
              cookieExpDays = options.resume.cookiesExpireIn;
  
          qq.setCookie(cookieName, cookieValue, cookieExpDays);
      }
  
      function deletePersistedChunkData(id) {
          if (fileState[id].file) {
              var cookieName = getChunkDataCookieName(id);
              qq.deleteCookie(cookieName);
          }
      }
  
      function getPersistedChunkData(id) {
          var chunkCookieValue = qq.getCookie(getChunkDataCookieName(id)),
              filename = api.getName(id),
              sections, uuid, partIndex, lastByteSent, initialRequestOverhead, estTotalRequestsSize;
  
          if (chunkCookieValue) {
              sections = chunkCookieValue.split(cookieItemDelimiter);
  
              if (sections.length === 5) {
                  uuid = sections[0];
                  partIndex = parseInt(sections[1], 10);
                  lastByteSent = parseInt(sections[2], 10);
                  initialRequestOverhead = parseInt(sections[3], 10);
                  estTotalRequestsSize = parseInt(sections[4], 10);
  
                  return {
                      uuid: uuid,
                      part: partIndex,
                      lastByteSent: lastByteSent,
                      initialRequestOverhead: initialRequestOverhead,
                      estTotalRequestsSize: estTotalRequestsSize
                  };
              }
              else {
                  log('Ignoring previously stored resume/chunk cookie for ' + filename + " - old cookie format", "warn");
              }
          }
      }
  
      function getChunkDataCookieName(id) {
          var filename = api.getName(id),
              fileSize = api.getSize(id),
              maxChunkSize = options.chunking.partSize,
              cookieName;
  
          cookieName = "qqfilechunk" + cookieItemDelimiter + encodeURIComponent(filename) + cookieItemDelimiter + fileSize + cookieItemDelimiter + maxChunkSize;
  
          if (resumeId !== undefined) {
              cookieName += cookieItemDelimiter + resumeId;
          }
  
          return cookieName;
      }
  
      function getResumeId() {
          if (options.resume.id !== null &&
              options.resume.id !== undefined &&
              !qq.isFunction(options.resume.id) &&
              !qq.isObject(options.resume.id)) {
  
              return options.resume.id;
          }
      }
  
      function handleFileChunkingUpload(id, retry) {
          var name = api.getName(id),
              firstChunkIndex = 0,
              persistedChunkInfoForResume, firstChunkDataForResume, currentChunkIndex;
  
          if (!fileState[id].remainingChunkIdxs || fileState[id].remainingChunkIdxs.length === 0) {
              fileState[id].remainingChunkIdxs = [];
  
              if (resumeEnabled && !retry && fileState[id].file) {
                  persistedChunkInfoForResume = getPersistedChunkData(id);
                  if (persistedChunkInfoForResume) {
                      firstChunkDataForResume = getChunkData(id, persistedChunkInfoForResume.part);
                      if (options.onResume(id, name, getChunkDataForCallback(firstChunkDataForResume)) !== false) {
                          firstChunkIndex = persistedChunkInfoForResume.part;
                          fileState[id].uuid = persistedChunkInfoForResume.uuid;
                          fileState[id].loaded = persistedChunkInfoForResume.lastByteSent;
                          fileState[id].estTotalRequestsSize = persistedChunkInfoForResume.estTotalRequestsSize;
                          fileState[id].initialRequestOverhead = persistedChunkInfoForResume.initialRequestOverhead;
                          fileState[id].attemptingResume = true;
                          log('Resuming ' + name + " at partition index " + firstChunkIndex);
                      }
                  }
              }
  
              for (currentChunkIndex = getTotalChunks(id)-1; currentChunkIndex >= firstChunkIndex; currentChunkIndex-=1) {
                  fileState[id].remainingChunkIdxs.unshift(currentChunkIndex);
              }
          }
  
          uploadNextChunk(id);
      }
  
      function handleStandardFileUpload(id) {
          var fileOrBlob = fileState[id].file || fileState[id].blobData.blob,
              name = api.getName(id),
              xhr, params, toSend;
  
          fileState[id].loaded = 0;
  
          xhr = createXhr(id);
  
          xhr.upload.onprogress = function(e){
              if (e.lengthComputable){
                  fileState[id].loaded = e.loaded;
                  options.onProgress(id, name, e.loaded, e.total);
              }
          };
  
          xhr.onreadystatechange = getReadyStateChangeHandler(id, xhr);
  
          params = options.paramsStore.getParams(id);
          toSend = setParamsAndGetEntityToSend(params, xhr, fileOrBlob, id);
          setHeaders(id, xhr);
  
          log('Sending upload request for ' + id);
          xhr.send(toSend);
      }
  
  
      api = {
          /**
           * Adds File or Blob to the queue
           * Returns id to use with upload, cancel
           **/
          add: function(fileOrBlobData){
              var id;
  
              if (fileOrBlobData instanceof File) {
                  id = fileState.push({file: fileOrBlobData}) - 1;
              }
              else if (fileOrBlobData.blob instanceof Blob) {
                  id = fileState.push({blobData: fileOrBlobData}) - 1;
              }
              else {
                  throw new Error('Passed obj in not a File or BlobData (in qq.UploadHandlerXhr)');
              }
  
              fileState[id].uuid = qq.getUniqueId();
              return id;
          },
          getName: function(id){
              if (api.isValid(id)) {
                  var file = fileState[id].file,
                      blobData = fileState[id].blobData;
  
                  if (file) {
                      // fix missing name in Safari 4
                      //NOTE: fixed missing name firefox 11.0a2 file.fileName is actually undefined
                      return (file.fileName !== null && file.fileName !== undefined) ? file.fileName : file.name;
                  }
                  else {
                      return blobData.name;
                  }
              }
              else {
                  log(id + " is not a valid item ID.", "error");
              }
          },
          getSize: function(id){
              /*jshint eqnull: true*/
              var fileOrBlob = fileState[id].file || fileState[id].blobData.blob;
  
              if (qq.isFileOrInput(fileOrBlob)) {
                  return fileOrBlob.fileSize != null ? fileOrBlob.fileSize : fileOrBlob.size;
              }
              else {
                  return fileOrBlob.size;
              }
          },
          getFile: function(id) {
              if (fileState[id]) {
                  return fileState[id].file || fileState[id].blobData.blob;
              }
          },
          /**
           * Returns uploaded bytes for file identified by id
           */
          getLoaded: function(id){
              return fileState[id].loaded || 0;
          },
          isValid: function(id) {
              return fileState[id] !== undefined;
          },
          reset: function() {
              fileState = [];
          },
          getUuid: function(id) {
              return fileState[id].uuid;
          },
          /**
           * Sends the file identified by id to the server
           */
          upload: function(id, retry){
              var name = this.getName(id);
  
              options.onUpload(id, name);
  
              if (chunkFiles) {
                  handleFileChunkingUpload(id, retry);
              }
              else {
                  handleStandardFileUpload(id);
              }
          },
          cancel: function(id){
              var xhr = fileState[id].xhr;
  
              options.onCancel(id, this.getName(id));
  
              if (xhr) {
                  xhr.onreadystatechange = null;
                  xhr.abort();
              }
  
              if (resumeEnabled) {
                  deletePersistedChunkData(id);
              }
  
              delete fileState[id];
          },
          getResumableFilesData: function() {
              var matchingCookieNames = [],
                  resumableFilesData = [];
  
              if (chunkFiles && resumeEnabled) {
                  if (resumeId === undefined) {
                      matchingCookieNames = qq.getCookieNames(new RegExp("^qqfilechunk\\" + cookieItemDelimiter + ".+\\" +
                          cookieItemDelimiter + "\\d+\\" + cookieItemDelimiter + options.chunking.partSize + "="));
                  }
                  else {
                      matchingCookieNames = qq.getCookieNames(new RegExp("^qqfilechunk\\" + cookieItemDelimiter + ".+\\" +
                          cookieItemDelimiter + "\\d+\\" + cookieItemDelimiter + options.chunking.partSize + "\\" +
                          cookieItemDelimiter + resumeId + "="));
                  }
  
                  qq.each(matchingCookieNames, function(idx, cookieName) {
                      var cookiesNameParts = cookieName.split(cookieItemDelimiter);
                      var cookieValueParts = qq.getCookie(cookieName).split(cookieItemDelimiter);
  
                      resumableFilesData.push({
                          name: decodeURIComponent(cookiesNameParts[1]),
                          size: cookiesNameParts[2],
                          uuid: cookieValueParts[0],
                          partIdx: cookieValueParts[1]
                      });
                  });
  
                  return resumableFilesData;
              }
              return [];
          }
      };
  
      return api;
  };
  /*globals jQuery, qq*/
  (function($) {
      "use strict";
      var uploader, $el, init, dataStore, pluginOption, pluginOptions, addCallbacks, transformVariables, isValidCommand,
          delegateCommand;
  
      pluginOptions = ['uploaderType'];
  
      init = function (options) {
          if (options) {
              var xformedOpts = transformVariables(options);
              addCallbacks(xformedOpts);
  
              if (pluginOption('uploaderType') === 'basic') {
                  uploader(new qq.FineUploaderBasic(xformedOpts));
              }
              else {
                  uploader(new qq.FineUploader(xformedOpts));
              }
          }
  
          return $el;
      };
  
      dataStore = function(key, val) {
          var data = $el.data('fineuploader');
  
          if (val) {
              if (data === undefined) {
                  data = {};
              }
              data[key] = val;
              $el.data('fineuploader', data);
          }
          else {
              if (data === undefined) {
                  return null;
              }
              return data[key];
          }
      };
  
      //the underlying Fine Uploader instance is stored in jQuery's data stored, associated with the element
      // tied to this instance of the plug-in
      uploader = function(instanceToStore) {
          return dataStore('uploader', instanceToStore);
      };
  
      pluginOption = function(option, optionVal) {
          return dataStore(option, optionVal);
      };
  
      //implement all callbacks defined in Fine Uploader as functions that trigger appropriately names events and
      // return the result of executing the bound handler back to Fine Uploader
      addCallbacks = function(transformedOpts) {
          var callbacks = transformedOpts.callbacks = {},
              uploaderInst = new qq.FineUploaderBasic();
  
          $.each(uploaderInst._options.callbacks, function(prop, func) {
              var name, $callbackEl;
  
              name = /^on(\w+)/.exec(prop)[1];
              name = name.substring(0, 1).toLowerCase() + name.substring(1);
              $callbackEl = $el;
  
              callbacks[prop] = function() {
                  var origFunc = func,
                      args = Array.prototype.slice.call(arguments),
                      jqueryHandlerResult = $callbackEl.triggerHandler(name, args);
  
                  if (jqueryHandlerResult === undefined &&
                          $.inArray(prop, uploaderInst.getPromissoryCallbackNames()) >= 0) {
                      return origFunc();
                  }
  
                  return jqueryHandlerResult;
              };
          });
      };
  
      //transform jQuery objects into HTMLElements, and pass along all other option properties
      transformVariables = function(source, dest) {
          var xformed, arrayVals;
  
          if (dest === undefined) {
              if (source.uploaderType !== 'basic') {
                  xformed = { element : $el[0] };
              }
              else {
                  xformed = {};
              }
          }
          else {
              xformed = dest;
          }
  
          $.each(source, function(prop, val) {
              if ($.inArray(prop, pluginOptions) >= 0) {
                  pluginOption(prop, val);
              }
              else if (val instanceof $) {
                  xformed[prop] = val[0];
              }
              else if ($.isPlainObject(val)) {
                  xformed[prop] = {};
                  transformVariables(val, xformed[prop]);
              }
              else if ($.isArray(val)) {
                  arrayVals = [];
                  $.each(val, function(idx, arrayVal) {
                      if (arrayVal instanceof $) {
                          $.merge(arrayVals, arrayVal);
                      }
                      else {
                          arrayVals.push(arrayVal);
                      }
                  });
                  xformed[prop] = arrayVals;
              }
              else {
                  xformed[prop] = val;
              }
          });
  
          if (dest === undefined) {
              return xformed;
          }
      };
  
      isValidCommand = function(command) {
          return $.type(command) === "string" &&
              !command.match(/^_/) && //enforce private methods convention
              uploader()[command] !== undefined;
      };
  
      //assuming we have already verified that this is a valid command, call the associated function in the underlying
      // Fine Uploader instance (passing along the arguments from the caller) and return the result of the call back to the caller
      delegateCommand = function(command) {
          var xformedArgs = [], origArgs = Array.prototype.slice.call(arguments, 1);
  
          transformVariables(origArgs, xformedArgs);
  
          return uploader()[command].apply(uploader(), xformedArgs);
      };
  
      $.fn.fineUploader = function(optionsOrCommand) {
          var self = this, selfArgs = arguments, retVals = [];
  
          this.each(function(index, el) {
              $el = $(el);
  
              if (uploader() && isValidCommand(optionsOrCommand)) {
                  retVals.push(delegateCommand.apply(self, selfArgs));
  
                  if (self.length === 1) {
                      return false;
                  }
              }
              else if (typeof optionsOrCommand === 'object' || !optionsOrCommand) {
                  init.apply(self, selfArgs);
              }
              else {
                  $.error('Method ' +  optionsOrCommand + ' does not exist on jQuery.fineUploader');
              }
          });
  
          if (retVals.length === 1) {
              return retVals[0];
          }
          else if (retVals.length > 1) {
              return retVals;
          }
  
          return this;
      };
  
  }(jQuery));
  