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