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