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