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