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, 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 text: 'Select a File', 261 // validation 262 allowedExtensions: [], 263 sizeLimit: 0, 264 minSizeLimit: 0, 265 // events 266 // return false to cancel submit 267 onSubmit: function(id, fileName){}, 268 onProgress: function(id, fileName, loaded, total){}, 269 onComplete: function(id, fileName, responseJSON){},
270 onCancel: function(id, fileName){}, 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 }); 341 342 return handler; 343 }, 344 _preventLeaveInProgress: function(){ 345 var self = this; 346 347 qq.attach(window, 'beforeunload', function(e){ 348 if (!self._filesInProgress){return;} 349 350 var e = e || window.event; 351 // for ie, ff 352 e.returnValue = self._options.messages.onLeave; 353 // for webkit 354 return self._options.messages.onLeave; 355 }); 356 }, 357 _onSubmit: function(id, fileName){ 358 this._filesInProgress++; 359 }, 360 _onProgress: function(id, fileName, loaded, total){ 361 }, 362 _onComplete: function(id, fileName, result){ 363 this._filesInProgress--; 364 if (result.error && result.error !== 0){ 365 this._options.showMessage(result.msg); 366 } 367 }, 368 _onCancel: function(id, fileName){ 369 this._filesInProgress--; 370 }, 371 _onInputChange: function(input){ 372 if (this._handler instanceof qq.UploadHandlerXhr){ 373 this._uploadFileList(input.files); 374 } else { 375 if (this._validateFile(input)){ 376 this._uploadFile(input); 377 } 378 } 379 this._button.reset(); 380 }, 381 _uploadFileList: function(files){ 382 for (var i=0; i<files.length; i++){
383 if ( !this._validateFile(files[i])){ 384 return; 385 } 386 } 387 388 for (var i=0; i<files.length; i++){ 389 this._uploadFile(files[i]); 390 } 391 }, 392 _uploadFile: function(fileContainer){ 393 var id = this._handler.add(fileContainer); 394 var fileName = this._handler.getName(id); 395 396 if (this._options.onSubmit(id, fileName) !== false){ 397 this._onSubmit(id, fileName); 398 this._handler.upload(id, this._options.params); 399 } 400 }, 401 _validateFile: function(file){ 402 var name, size; 403 404 if (file.value){ 405 // it is a file input 406 // get input value and remove path to normalize 407 name = file.value.replace(/.*(\/|\\)/, ""); 408 } else { 409 // fix missing properties in Safari 410 name = file.fileName != null ? file.fileName : file.name; 411 size = file.fileSize != null ? file.fileSize : file.size; 412 } 413 414 if (! this._isAllowedExtension(name)){ 415 this._error('typeError', name); 416 return false; 417 418 } else if (size === 0){ 419 this._error('emptyError', name); 420 return false; 421 422 } else if (size && this._options.sizeLimit && size > this._options.sizeLimit){ 423 this._error('sizeError', name); 424 return false; 425 426 } else if (size && size < this._options.minSizeLimit){ 427 this._error('minSizeError', name); 428 return false; 429 } 430 431 return true; 432 }, 433 _error: function(code, fileName){ 434 var message = this._options.messages[code]; 435 function r(name, replacement){ message = message.replace(name, replacement); } 436 437 r('{file}', this._formatFileName(fileName)); 438 r('{extensions}', this._options.allowedExtensions.join(', ')); 439 r('{sizeLimit}', this._formatSize(this._options.sizeLimit)); 440 r('{minSizeLimit}', this._formatSize(this._options.minSizeLimit)); 441 442 this._options.showMessage(message); 443 }, 444 _formatFileName: function(name){ 445 if (name.length > 33){ 446 name = name.slice(0, 19) + '...' + name.slice(-13); 447 } 448 return name; 449 }, 450 _isAllowedExtension: function(fileName){ 451 var ext = (-1 !== fileName.indexOf('.')) ? fileName.replace(/.*[.]/, '').toLowerCase() : ''; 452 var allowed = this._options.allowedExtensions; 453 454 if (!allowed.length){return true;} 455 456 for (var i=0; i<allowed.length; i++){ 457 if (allowed[i].toLowerCase() == ext){ return true;} 458 } 459 460 return false; 461 }, 462 _formatSize: function(bytes){ 463 var i = -1; 464 do { 465 bytes = bytes / 1024; 466 i++; 467 } while (bytes > 99); 468 469 return Math.max(bytes, 0.1).toFixed(1) + ['kB', 'MB', 'GB', 'TB', 'PB', 'EB'][i]; 470 } 471}; 472 473 474/** 475 * Class that creates upload widget with drag-and-drop and file list 476 * @inherits qq.FileUploaderBasic 477 */ 478qq.FileUploader = function(o){ 479 // call parent constructor 480 qq.FileUploaderBasic.apply(this, arguments); 481 482 // additional options 483 qq.extend(this._options, { 484 element: null, 485 // if set, will be used instead of qq-upload-list in template 486 listElement: null, 487 488 template: '<div class="qq-uploader">' + 489 '<div class="qq-upload-drop-area"><span>Drop files here to upload</span></div>' + 490 '<div class="qq-upload-button"> '+this._options.text+' </div>' + 491 '<ul class="qq-upload-list"></ul>' + 492 '</div>', 493 494 // template for one item in file list 495 fileTemplate: '<li>' + 496 '<span class="qq-upload-file"></span>' + 497 '<span class="qq-upload-spinner"></span>' + 498 '<span class="qq-upload-size"></span>' + 499 '<a class="qq-upload-cancel" href="#">Cancel</a>' + 500 '<span class="qq-upload-failed-text">Failed</span>' + 501 '</li>', 502 503 classes: { 504 // used to get elements from templates 505 button: 'qq-upload-button', 506 drop: 'qq-upload-drop-area', 507 dropActive: 'qq-upload-drop-area-active', 508 list: 'qq-upload-list', 509 510 file: 'qq-upload-file', 511 spinner: 'qq-upload-spinner', 512 size: 'qq-upload-size', 513 cancel: 'qq-upload-cancel', 514 515 // added to list item when upload completes 516 // used in css to hide progress spinner 517 success: 'qq-upload-success', 518 fail: 'qq-upload-fail' 519 } 520 }); 521 // overwrite options with user supplied 522 qq.extend(this._options, o); 523 524 this._element = this._options.element; 525 this._element.innerHTML = this._options.template; 526 this._listElement = this._options.listElement || this._find(this._element, 'list'); 527 528 this._classes = this._options.classes; 529 530 this._button = this._createUploadButton(this._find(this._element, 'button')); 531 532 this._bindCancelEvent(); 533 this._setupDragDrop(); 534}; 535 536// inherit from Basic Uploader 537qq.extend(qq.FileUploader.prototype, qq.FileUploaderBasic.prototype); 538 539qq.extend(qq.FileUploader.prototype, { 540 /** 541 * Gets one of the elements listed in this._options.classes 542 **/ 543 _find: function(parent, type){ 544 var element = qq.getByClass(parent, this._options.classes[type])[0]; 545 if (!element){ 546 throw new Error('element not found ' + type); 547 } 548 549 return element; 550 }, 551 _setupDragDrop: function(){ 552 var self = this, 553 dropArea = this._find(this._element, 'drop'); 554 555 var dz = new qq.UploadDropZone({ 556 element: dropArea, 557 onEnter: function(e){ 558 qq.addClass(dropArea, self._classes.dropActive); 559 e.stopPropagation(); 560 }, 561 onLeave: function(e){ 562 e.stopPropagation(); 563 }, 564 onLeaveNotDescendants: function(e){ 565 qq.removeClass(dropArea, self._classes.dropActive); 566 }, 567 onDrop: function(e){ 568 dropArea.style.display = 'none'; 569 qq.removeClass(dropArea, self._classes.dropActive); 570 self._uploadFileList(e.dataTransfer.files); 571 } 572 }); 573 574 dropArea.style.display = 'none'; 575 576 qq.attach(document, 'dragenter', function(e){ 577 if (!dz._isValidFileDrag(e)) return; 578 579 dropArea.style.display = 'block'; 580 }); 581 qq.attach(document, 'dragleave', function(e){ 582 if (!dz._isValidFileDrag(e)) return; 583 584 var relatedTarget = document.elementFromPoint(e.clientX, e.clientY); 585 // only fire when leaving document out 586 if ( ! relatedTarget || relatedTarget.nodeName == "HTML"){ 587 dropArea.style.display = 'none'; 588 } 589 }); 590 }, 591 _onSubmit: function(id, fileName){ 592 qq.FileUploaderBasic.prototype._onSubmit.apply(this, arguments); 593 this._addToList(id, fileName); 594 }, 595 _onProgress: function(id, fileName, loaded, total){ 596 qq.FileUploaderBasic.prototype._onProgress.apply(this, arguments); 597 598 var item = this._getItemByFileId(id); 599 var size = this._find(item, 'size'); 600 size.style.display = 'inline'; 601 602 var text; 603 if (loaded != total){ 604 text = Math.round(loaded / total * 100) + '% from ' + this._formatSize(total); 605 } else { 606 text = this._formatSize(total); 607 } 608 609 qq.setText(size, text); 610 }, 611 _onComplete: function(id, fileName, result){ 612 qq.FileUploaderBasic.prototype._onComplete.apply(this, arguments); 613 614 // mark completed 615 var item = this._getItemByFileId(id); 616 qq.remove(this._find(item, 'cancel')); 617 qq.remove(this._find(item, 'spinner')); 618 619 if (result.success){ 620 qq.addClass(item, this._classes.success); 621 } else { 622 qq.addClass(item, this._classes.fail); 623 } 624 }, 625 _addToList: function(id, fileName){ 626 var item = qq.toElement(this._options.fileTemplate); 627 item.qqFileId = id; 628 629 var fileElement = this._find(item, 'file'); 630 qq.setText(fileElement, this._formatFileName(fileName)); 631 this._find(item, 'size').style.display = 'none'; 632 633 this._listElement.appendChild(item); 634 this._listElement.style.display = 'block'; 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){ 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});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.