[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/lib/scripts/ -> fileuploader.js (source)

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