b2evolution PHP Cross Reference Blogging Systems

Source: /rsc/js/multiupload/fileuploader.js - 1369 lines - 41108 bytes - Summary - Text - Print

Description: http://github.com/valums/file-uploader

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

title

Description

title

Description

title

Description

title

title

Body