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 14var qq = qq || {}; 15 16/** 17 * Adds all missing properties from second obj to first obj 18 */ 19qq.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 */ 29qq.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 45qq.getUniqueId = (function(){ 46 var id = 0; 47 return function(){ return id++; }; 48})(); 49 50// 51// Events 52 53qq.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}; 60qq.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 68qq.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 */ 82qq.insertBefore = function(a, b){ 83 b.parentNode.insertBefore(a, b); 84}; 85qq.remove = function(element){ 86 element.parentNode.removeChild(element); 87}; 88 89qq.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 */ 104qq.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 */ 121qq.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}; 129qq.hasClass = function(element, name){ 130 var re = new RegExp('(^| )' + name + '( |$)'); 131 return re.test(element.className); 132}; 133qq.addClass = function(element, name){ 134 if (!qq.hasClass(element, name)){ 135 element.className += ' ' + name; 136 } 137}; 138qq.removeClass = function(element, name){ 139 var re = new RegExp('(^| )' + name + '( |$)'); 140 element.className = element.className.replace(re, ' ').replace(/^\s+|\s+$/g, ""); 141}; 142qq.setText = function(element, text){ 143 element.innerText = text; 144 element.textContent = text; 145}; 146 147// 148// Selecting elements 149 150qq.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 164qq.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 */ 197qq.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 248var qq = qq || {}; 249 250/** 251 * Creates upload button, validates upload, but doesn't create file list or dd. 252 */ 253qq.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 297qq.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 */ 479qq.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 538qq.extend(qq.FileUploader.prototype, qq.FileUploaderBasic.prototype); 539 540qq.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 668qq.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 685qq.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 755qq.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 783qq.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 */ 857qq.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}; 873qq.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 */ 956qq.UploadHandlerForm = function(o){ 957 qq.UploadHandlerAbstract.apply(this, arguments); 958 959 this._inputs = {}; 960}; 961// @inherits qq.UploadHandlerAbstract 962qq.extend(qq.UploadHandlerForm.prototype, qq.UploadHandlerAbstract.prototype); 963 964qq.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 */ 1119qq.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 1130qq.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 1141qq.extend(qq.UploadHandlerXhr.prototype, qq.UploadHandlerAbstract.prototype); 1142 1143qq.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});