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} 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 // Labels are text, including when read from untrusted data-* attributes. 578 // Replace all tokens in one pass so label text cannot introduce new tokens. 579 var options = this._options; 580 var replaceText = function(match, name) { 581 var value = options[name === 'failUploadtext' ? 'failUploadText' : name]; 582 var text = document.createElement('span'); 583 qq.setText(text, value == null ? '' : String(value)); 584 return text.innerHTML; 585 }; 586 this._options.template = this._options.template.replace(/\{(
586dragText|uploadButtonText)\}/g, replaceText); 587 this._options.fileTemplate = this._options.fileTemplate.replace(/\{(cancelButtonText|failUploadtext)\}/g, replaceText); 588 589 this._element = this._options.element; 590 this._element.innerHTML = this._options.template; 591 this._listElement = this._options.listElement || this._find(this._element, 'list'); 592 593 this._classes = this._options.classes; 594 595 this._button = this._createUploadButton(this._find(this._element, 'button')); 596 597 this._bindCancelEvent(); 598 this._setupDragDrop(); 599}; 600 601// inherit from Basic Uploader 602qq.extend(qq.FileUploader.prototype, qq.FileUploaderBasic.prototype); 603 604qq.extend(qq.FileUploader.prototype, { 605 addExtraDropzone: function(element){ 606 this._setupExtraDropzone(element); 607 }, 608 removeExtraDropzone: function(element){ 609 var dzs = this._options.extraDropzones; 610 for(var i in dzs) if (dzs[i] === element) return this._options.extraDropzones.splice(i,1); 611 }, 612 _leaving_document_out: function(e){ 613 return ((qq.chrome() || (qq.safari() && qq.windows())) && e.clientX == 0 && e.clientY == 0) // null coords for Chrome and Safari Windows 614 || (qq.firefox() && !e.relatedTarget); // null e.relatedTarget for Firefox 615 }, 616 /** 617 * Gets one of the elements listed in this._options.classes 618 **/ 619 _find: function(parent, type){ 620 var element = qq.getByClass(parent, this._options.classes[type])[0]; 621 if (!element){ 622 throw new Error('element not found ' + type); 623 } 624 625 return element; 626 }, 627 _setupExtraDropzone: function(element){ 628 this._options.extraDropzones.push(element); 629 this._setupDropzone(element); 630 }, 631 _setupDropzone: function(dropArea){ 632 var self = this; 633 634 var dz = new qq.UploadDropZone({ 635 element: dropArea, 636 onEnter: function(e){ 637 qq.addClass(dropArea, self._classes.dropActive); 638 e.stopPropagation(); 639 }, 640 onLeave: function(e){ 641 //e.stopPropagation(); 642 }, 643 onLeaveNotDescendants: function(e){ 644 qq.removeClass(dropArea, self._classes.dropActive); 645 }, 646 onDrop: function(e){ 647 dropArea.style.display = 'none'; 648 qq.removeClass(dropArea, self._classes.dropActive); 649 self._uploadFileList(e.dataTransfer.files); 650 } 651 }); 652 653 this.addDisposer(function() { dz.dispose(); }); 654 655 dropArea.style.display = 'none'; 656 }, 657 _setupDragDrop: function(){ 658 var dropArea = this._find(this._element, 'drop'); 659 var self = this; 660 this._options.extraDropzones.push(dropArea); 661 662 var dropzones = this._options.extraDropzones; 663 var i; 664 for (i=0; i < dropzones.length; i++){ 665 this._setupDropzone(dropzones[i]); 666 } 667 668 // IE <= 9 does not support the File API used for drag+drop uploads 669 // Any volunteers to enable & test this for IE10? 670 if (!qq.ie()) { 671 this._attach(document, 'dragenter', function(e){ 672 // console.log(); 673 if (!self._isValidFileDrag(e)) return; // now causing error. Need it be here? 674 if (qq.hasClass(dropArea, self._classes.dropDisabled)) return; 675 676 dropArea.style.display = 'block'; 677 for (i=0; i < dropzones.length; i++){ dropzones[i].style.display = 'block'; } 678 679 }); 680 } 681 this._attach(document, 'dragleave', function(e){ 682 var relatedTarget = document.elementFromPoint(e.clientX, e.clientY); 683 // only fire when leaving document out 684 if (qq.FileUploader.prototype._leaving_document_out(e)) { 685 for (i=0; i < dropzones.length; i++){ dropzones[i].style.display = 'none'; } 686 } 687 }); 688 qq.attach(document, 'drop', function(e){ 689 for (i=0; i < dropzones.length; i++){ dropzones[i].style.display = 'none'; } 690 e.preventDefault(); 691 }); 692 }, 693 _onSubmit: function(id, fileName){ 694 qq.FileUploaderBasic.prototype._onSubmit.apply(this, arguments); 695 this._addToList(id, fileName); 696 },
697 // Update the progress bar & percentage as the file is uploaded 698 _onProgress: function(id, fileName, loaded, total){ 699 qq.FileUploaderBasic.prototype._onProgress.apply(this, arguments); 700 701 var item = this._getItemByFileId(id); 702 var size = this._find(item, 'size'); 703 size.style.display = 'inline'; 704 705 var text; 706 var percent = Math.round(loaded / total * 100); 707 708 if (loaded != total) { 709 // If still uploading, display percentage 710 text = percent + '% from ' + this._formatSize(total); 711 } else { 712 // If complete, just display final size 713 text = this._formatSize(total); 714 } 715 716 // Update progress bar <span> tag 717 this._find(item, 'progressBar').style.width = percent + '%'; 718 719 qq.setText(size, text); 720 }, 721 _onComplete: function(id, fileName, result){ 722 qq.FileUploaderBasic.prototype._onComplete.apply(this, arguments); 723 724 // mark completed 725 var item = this._getItemByFileId(id); 726 qq.remove(this._find(item, 'cancel')); 727 qq.remove(this._find(item, 'spinner')); 728 qq.remove(item); 729 730 if (result.success){ 731 qq.addClass(item, this._classes.success); 732 } else { 733 qq.addClass(item, this._classes.fail); 734 } 735 }, 736 _addToList: function(id, fileName){ 737 var item = qq.toElement(this._options.fileTemplate); 738 item.qqFileId = id; 739 740 var fileElement = this._find(item, 'file'); 741 qq.setText(fileElement, this._formatFileName(fileName)); 742 this._find(item, 'size').style.display = 'none'; 743 if (!this._options.multiple) this._clearList(); 744 this._listElement.appendChild(item); 745 }, 746 _clearList: function(){ 747 this._listElement.innerHTML = ''; 748 }, 749 _getItemByFileId: function(id){ 750 var item = this._listElement.firstChild; 751 752 // there can't be txt nodes in dynamically created list 753 // and we can use nextSibling 754 while (item){ 755 if (item.qqFileId == id) return item; 756 item = item.nextSibling; 757 } 758 }, 759 /** 760 * delegate click event for cancel link 761 **/ 762 _bindCancelEvent: function(){ 763 var self = this, 764 list = this._listElement; 765 766 this._attach(list, 'click', function(e){ 767 e = e || window.event; 768 var target = e.target || e.srcElement; 769 770 if (qq.hasClass(target, self._classes.cancel)){ 771 qq.preventDefault(e); 772 773 var item = target.parentNode; 774 self._handler.cancel(item.qqFileId); 775 qq.remove(item); 776 } 777 }); 778 } 779}); 780 781qq.UploadDropZone = function(o){ 782 this._options = { 783 element: null, 784 onEnter: function(e){}, 785 onLeave: function(e){}, 786 // is not fired when leaving element by hovering descendants 787 onLeaveNotDescendants: function(e){}, 788 onDrop: function(e){} 789 }; 790 qq.extend(this._options, o); 791 qq.extend(this, qq.DisposeSupport); 792 793 this._element = this._options.element; 794 795 this._disableDropOutside(); 796 this._attachEvents(); 797}; 798 799qq.UploadDropZone.prototype = { 800 _dragover_should_be_canceled: function(){ 801 return qq.safari() || (qq.firefox() && qq.windows()); 802 }, 803 _disableDropOutside: function(e){ 804 // run only once for all instances 805 if (!qq.UploadDropZone.dropOutsideDisabled ){ 806 807 // for these cases we need to catch onDrop to reset dropArea 808 if (this._dragover_should_be_canceled){ 809 qq.attach(document, 'dragover', function(e){ 810 e.preventDefault(); 811 }); 812 } else { 813 qq.attach(document, 'dragover', function(e){ 814 if (e.dataTransfer){ 815 e.dataTransfer.dropEffect = 'none'; 816 e.preventDefault(); 817 } 818 }); 819 } 820 821 qq.UploadDropZone.dropOutsideDisabled = true; 822 } 823 }, 824 _attachEvents: function(){ 825 var self = this; 826 827 self._attach(self._element, 'dragover', function(e){ 828 if (!self._isValidFileDrag(e)) return; 829 830 var effect = qq.ie() ? null : e.dataTransfer.effectAllowed; 831 if (effect == 'move' || effect == 'linkMove'){ 832 e.dataTransfer.dropEffect = 'move'; // for FF (only move allowed) 833 } else { 834 e.dataTransfer.dropEffect = 'copy'; // for Chrome 835 } 836 837 e.stopPropagation(); 838 e.preventDefault(); 839 }); 840 841 self._attach(self._element, 'dragenter', function(e){ 842 if (!self._isValidFileDrag(e)) return; 843 844 self._options.onEnter(e); 845 }); 846 847 self._attach(self._element, 'dragleave', function(e){ 848 if (!self._isValidFileDrag(e)) return; 849 850 self._options.onLeave(e); 851 852 var relatedTarget = document.elementFromPoint(e.clientX, e.clientY); 853 // do not fire when moving a mouse over a descendant 854 if (qq.contains(this, relatedTarget)) return; 855 856 self._options.onLeaveNotDescendants(e); 857 }); 858 859 self._attach(self._element, 'drop', function(e){ 860 if (!self._isValidFileDrag(e)) return; 861 862 e.preventDefault(); 863 self._options.onDrop(e); 864 }); 865 }, 866 _isValidFileDrag: function(e){ 867 // e.dataTransfer currently causing IE errors 868 // IE9 does NOT support file API, so drag-and-drop is not possible 869 // IE10 should work, but currently has not been tested - any volunteers? 870 if (qq.ie()) return false;
871 872 var dt = e.dataTransfer, 873 // do not check dt.types.contains in webkit, because it crashes safari 4 874 isSafari = qq.safari(); 875 876 // dt.effectAllowed is none in Safari 5 877 // dt.types.contains check is for firefox 878 return dt && dt.effectAllowed != 'none' && 879 (dt.files || (!isSafari && dt.types.contains && dt.types.contains('Files'))); 880 881 } 882}; 883 884qq.UploadButton = function(o){ 885 this._options = { 886 element: null, 887 // if set to true adds multiple attribute to file input 888 multiple: false, 889 acceptFiles: null, 890 // name attribute of file input 891 name: 'file', 892 onChange: function(input){}, 893 hoverClass: 'qq-upload-button-hover', 894 focusClass: 'qq-upload-button-focus' 895 }; 896 897 qq.extend(this._options, o); 898 qq.extend(this, qq.DisposeSupport); 899 900 this._element = this._options.element; 901 902 // make button suitable container for input 903 qq.css(this._element, { 904 position: 'relative', 905 overflow: 'hidden', 906 // Make sure browse button is in the right side 907 // in Internet Explorer 908 direction: 'ltr' 909 }); 910 911 this._input = this._createInput(); 912}; 913 914qq.UploadButton.prototype = { 915 /* returns file input element */ 916 getInput: function(){ 917 return this._input; 918 }, 919 /* cleans/recreates the file input */ 920 reset: function(){ 921 if (this._input.parentNode){ 922 qq.remove(this._input); 923 } 924 925 qq.removeClass(this._element, this._options.focusClass); 926 this._input = this._createInput(); 927 }, 928 _createInput: function(){ 929 var input = document.createElement("input"); 930 931 if (this._options.multiple){ 932 input.setAttribute("multiple", "multiple"); 933 } 934 935 if (this._options.acceptFiles) input.setAttribute("accept", this._options.acceptFiles); 936 937 input.setAttribute("type", "file"); 938 input.setAttribute("name", this._options.name); 939 940 qq.css(input, { 941 position: 'absolute', 942 // in Opera only 'browse' button 943 // is clickable and it is located at 944 // the right side of the input 945 right: 0, 946 top: 0, 947 fontFamily: 'Arial', 948 // 4 persons reported this, the max values that worked for them were 243, 236, 236, 118 949 fontSize: '118px', 950 margin: 0, 951 padding: 0, 952 cursor: 'pointer', 953 opacity: 0 954 }); 955 956 this._element.appendChild(input); 957 958 var self = this; 959 this._attach(input, 'change', function(){ 960 self._options.onChange(input); 961 }); 962 963 this._attach(input, 'mouseover', function(){ 964 qq.addClass(self._element, self._options.hoverClass); 965 }); 966 this._attach(input, 'mouseout', function(){ 967 qq.removeClass(self._element, self._options.hoverClass); 968 }); 969 this._attach(input, 'focus', function(){ 970 qq.addClass(self._element, self._options.focusClass); 971 }); 972 this._attach(input, 'blur', function(){ 973 qq.removeClass(self._element, self._options.focusClass); 974 }); 975 976 // IE and Opera, unfortunately have 2 tab stops on file input 977 // which is unacceptable in our case, disable keyboard access 978 if (window.attachEvent){ 979 // it is IE or Opera 980 input.setAttribute('tabIndex', "-1"); 981 } 982 983 return input; 984 } 985}; 986 987/** 988 * Class for uploading files, uploading itself is handled by child classes 989 */ 990qq.UploadHandlerAbstract = function(o){ 991 // Default options, can be overridden by the user 992 this._options = { 993 debug: false, 994 action: '/upload.php', 995 // maximum number of concurrent uploads 996 maxConnections: 999, 997 onProgress: function(id, fileName, loaded, total){}, 998 onComplete: function(id, fileName, response){}, 999 onCancel: function(id, fileName){}, 1000 onUpload: function(id, fileName, xhr){} 1001 }; 1002 qq.extend(this._options, o); 1003 1004 this._queue = []; 1005 // params for files in queue 1006 this._params = []; 1007}; 1008qq.UploadHandlerAbstract.prototype = { 1009 log: function(str){ 1010 if (this._options.debug && window.console) console.log('[uploader] ' + str); 1011 }, 1012 /** 1013 * Adds file or file input to the queue 1014 * @returns id 1015 **/ 1016 add: function(file){}, 1017 /** 1018 * Sends the file identified by id and additional query params to the server 1019 */ 1020 upload: function(id, params){ 1021 var len = this._queue.push(id); 1022 1023 var copy = {}; 1024 qq.extend(copy, params); 1025 this._params[id] = copy; 1026 1027 // if too many active uploads, wait... 1028 if (len <= this._options.maxConnections){ 1029 this._upload(id, this._params[id]); 1030 } 1031 }, 1032 /** 1033 * Cancels file upload by id 1034 */ 1035 cancel: function(id){ 1036 this._cancel(id); 1037 this._dequeue(id); 1038 }, 1039 /** 1040 * Cancells all uploads 1041 */ 1042 cancelAll: function(){ 1043 for (var i=0; i<this._queue.length; i++){ 1044 this._cancel(this._queue[i]); 1045 } 1046 this._queue = []; 1047 }, 1048 /** 1049 * Returns name of the file identified by id 1050 */ 1051 getName: function(id){}, 1052 /** 1053 * Returns size of the file identified by id 1054 */ 1055 getSize: function(id){}, 1056 /** 1057 * Returns id of files being uploaded or 1058 * waiting for their turn 1059 */ 1060 getQueue: function(){ 1061 return this._queue; 1062 }, 1063 /** 1064 * Actual upload method 1065 */ 1066 _upload: function(id){}, 1067 /** 1068 * Actual cancel method 1069 */ 1070 _cancel: function(id){}, 1071 /** 1072 * Removes element from queue, starts upload of next 1073 */ 1074 _dequeue: function(id){ 1075 var i = qq.indexOf(this._queue, id); 1076 this._queue.splice(i, 1); 1077 1078 var max = this._options.maxConnections; 1079 1080 if (this._queue.length >= max && i < max){ 1081 var nextId = this._queue[max-1]; 1082 this._upload(nextId, this._params[nextId]); 1083 } 1084 } 1085}; 1086 1087/** 1088 * Class for uploading files using form and iframe 1089 * @inherits qq.UploadHandlerAbstract 1090 */ 1091qq.UploadHandlerForm = function(o){ 1092 qq.UploadHandlerAbstract.apply(this, arguments); 1093 1094 this._inputs = {}; 1095}; 1096// @inherits qq.UploadHandlerAbstract 1097qq.extend(qq.UploadHandlerForm.prototype, qq.UploadHandlerAbstract.prototype); 1098 1099qq.extend(qq.UploadHandlerForm.prototype, { 1100 add: function(fileInput){ 1101 fileInput.setAttribute('name', this._options.inputName); 1102 var id = 'qq-upload-handler-iframe' + qq.getUniqueId(); 1103 1104 this._inputs[id] = fileInput; 1105 1106 // remove file input from DOM 1107 if (fileInput.parentNode){ 1108 qq.remove(fileInput); 1109 } 1110 1111 return id; 1112 }, 1113 getName: function(id){ 1114 // get input value and remove path to normalize 1115 return this._inputs[id].value.replace(/.*(\/|\\)/, ""); 1116 }, 1117 _cancel: function(id){ 1118 this._options.onCancel(id, this.getName(id)); 1119 1120 delete this._inputs[id]; 1121 1122 var iframe = document.getElementById(id); 1123 if (iframe){ 1124 // to cancel request set src to something else 1125 // we use src="javascript:false;" because it doesn't 1126 // trigger ie6 prompt on https 1127 iframe.setAttribute('src', 'javascript:false;'); 1128 1129 qq.remove(iframe); 1130 } 1131 }, 1132 _upload: function(id, params){ 1133 this._options.onUpload(id, this.getName(id), false); 1134 var input = this._inputs[id]; 1135 1136 if (!input){
1137 throw new Error('file with passed id was not added, or already uploaded or cancelled'); 1138 } 1139 1140 var fileName = this.getName(id); 1141 1142 var iframe = this._createIframe(id); 1143 var form = this._createForm(iframe, params); 1144 form.appendChild(input); 1145 1146 var self = this; 1147 this._attachLoadEvent(iframe, function(){ 1148 self.log('iframe loaded'); 1149 1150 var response = self._getIframeContentJSON(iframe); 1151 1152 self._options.onComplete(id, fileName, response); 1153 self._dequeue(id); 1154 1155 delete self._inputs[id]; 1156 // timeout added to fix busy state in FF3.6 1157 setTimeout(function(){ 1158 self._detach_event(); 1159 qq.remove(iframe); 1160 }, 1); 1161 }); 1162 1163 form.submit(); 1164 qq.remove(form); 1165 1166 return id; 1167 }, 1168 _attachLoadEvent: function(iframe, callback){ 1169 this._detach_event = qq.attach(iframe, 'load', function(){ 1170 // when we remove iframe from dom 1171 // the request stops, but in IE load 1172 // event fires 1173 if (!iframe.parentNode){ 1174 return; 1175 } 1176 1177 // fixing Opera 10.53 1178 if (iframe.contentDocument && 1179 iframe.contentDocument.body && 1180 iframe.contentDocument.body.innerHTML == "false"){ 1181 // In Opera event is fired second time 1182 // when body.innerHTML changed from false 1183 // to server response approx. after 1 sec 1184 // when we upload file with iframe 1185 return; 1186 } 1187 1188 callback(); 1189 }); 1190 }, 1191 /** 1192 * Returns json object received by iframe from server. 1193 */ 1194 _getIframeContentJSON: function(iframe){ 1195 // iframe.contentWindow.document - for IE<7 1196 var doc = iframe.contentDocument ? iframe.contentDocument: iframe.contentWindow.document, 1197 response; 1198 1199 var innerHTML = doc.body.innerHTML; 1200 this.log("converting iframe's innerHTML to JSON"); 1201 this.log("innerHTML = " + innerHTML); 1202 //plain text response may be wrapped in <pre> tag 1203 if (innerHTML.slice(0, 5).toLowerCase() == '<pre>' && innerHTML.slice(-6).toLowerCase() == '</pre>') { 1204 innerHTML = doc.body.firstChild.firstChild.nodeValue; 1205 } 1206 1207 try { 1208 response = eval("(" + innerHTML + ")"); 1209 } catch(err){ 1210 response = {}; 1211 } 1212 1213 return response; 1214 }, 1215 /** 1216 * Creates iframe with unique name 1217 */ 1218 _createIframe: function(id){ 1219 // We can't use following code as the name attribute 1220 // won't be properly registered in IE6, and new window 1221 // on form submit will open 1222 // var iframe = document.createElement('iframe'); 1223 // iframe.setAttribute('name', id); 1224 1225 var iframe = qq.toElement('<iframe src="javascript:false;" name="' + id + '" />'); 1226 // src="javascript:false;" removes ie6 prompt on https 1227 1228 iframe.setAttribute('id', id); 1229 1230 iframe.style.display = 'none'; 1231 document.body.appendChild(iframe); 1232 1233 return iframe; 1234 }, 1235 /** 1236 * Creates form, that will be submitted to iframe 1237 */ 1238 _createForm: function(iframe, params){ 1239 // We can't use the following code in IE6 1240 // var form = document.createElement('form'); 1241 // form.setAttribute('method', 'post'); 1242 // form.setAttribute('enctype', 'multipart/form-data'); 1243 // Because in this case file won't be attached to request 1244 var form = qq.toElement('<form method="post" enctype="multipart/form-data"></form>'); 1245 1246 var queryString = qq.obj2url(params, this._options.action); 1247 1248 form.setAttribute('action', queryString); 1249 form.setAttribute('target', iframe.name); 1250 form.style.display = 'none'; 1251 document.body.appendChild(form); 1252 1253 return form; 1254 } 1255}); 1256 1257/** 1258 * Class for uploading files using xhr 1259 * @inherits qq.UploadHandlerAbstract 1260 */ 1261qq.UploadHandlerXhr = function(o){ 1262 qq.UploadHandlerAbstract.apply(this, arguments); 1263 1264 this._files = []; 1265 this._xhrs = []; 1266 1267 // current loaded size in bytes for each file 1268 this._loaded = []; 1269}; 1270 1271// static method 1272qq.UploadHandlerXhr.isSupported = function(){ 1273 var input = document.createElement('input'); 1274 input.type = 'file'; 1275 1276 return ( 1277 'multiple' in input && 1278 typeof File != "undefined" && 1279 typeof FormData != "undefined" && 1280 typeof (new XMLHttpRequest()).upload != "undefined" ); 1281}; 1282 1283// @inherits qq.UploadHandlerAbstract 1284qq.extend(qq.UploadHandlerXhr.prototype, qq.UploadHandlerAbstract.prototype) 1285 1286qq.extend(qq.UploadHandlerXhr.prototype, { 1287 /** 1288 * Adds file to the queue 1289 * Returns id to use with upload, cancel 1290 **/ 1291 add: function(file){ 1292 if (!(file instanceof File)){ 1293 throw new Error('Passed obj in not a File (in qq.UploadHandlerXhr)'); 1294 } 1295 1296 return this._files.push(file) - 1; 1297 }, 1298 getName: function(id){ 1299 var file = this._files[id]; 1300 // fix missing name in Safari 4 1301 //NOTE: fixed missing name firefox 11.0a2 file.fileName is actually undefined 1302 return (file.fileName !== null && file.fileName !== undefined) ? file.fileName : file.name; 1303 }, 1304 getSize: function(id){ 1305 var file = this._files[id]; 1306 return file.fileSize != null ? file.fileSize : file.size; 1307 }, 1308 /** 1309 * Returns uploaded bytes for file identified by id 1310 */ 1311 getLoaded: function(id){ 1312 return this._loaded[id] || 0; 1313 }, 1314 /** 1315 * Sends the file identified by id and additional query params to the server 1316 * @param {Object} params name-value string pairs 1317 */ 1318 _upload: function(id, params){ 1319 this._options.onUpload(id, this.getName(id), true); 1320 1321 var file = this._files[id], 1322 name = this.getName(id),
1323 size = this.getSize(id); 1324 1325 this._loaded[id] = 0; 1326 1327 var xhr = this._xhrs[id] = new XMLHttpRequest(); 1328 var self = this; 1329 1330 xhr.upload.onprogress = function(e){ 1331 if (e.lengthComputable){ 1332 self._loaded[id] = e.loaded; 1333 self._options.onProgress(id, name, e.loaded, e.total); 1334 } 1335 }; 1336 1337 xhr.onreadystatechange = function(){ 1338 if (xhr.readyState == 4){ 1339 self._onComplete(id, xhr); 1340 } 1341 }; 1342 1343 // build query string 1344 params = params || {}; 1345 params[this._options.inputName] = name; 1346 var queryString = qq.obj2url(params, this._options.action); 1347 1348 xhr.open("POST", queryString, true); 1349 xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); 1350 xhr.setRequestHeader("X-File-Name", encodeURIComponent(name)); 1351 if (this._options.encoding == 'multipart') { 1352 var formData = new FormData(); 1353 formData.append(name, file); 1354 file = formData; 1355 } else { 1356 xhr.setRequestHeader("Content-Type", "application/octet-stream"); 1357 //NOTE: return mime type in xhr works on chrome 16.0.9 firefox 11.0a2 1358 xhr.setRequestHeader("X-Mime-Type",file.type ); 1359 } 1360 for (key in this._options.customHeaders){ 1361 xhr.setRequestHeader(key, this._options.customHeaders[key]); 1362 }; 1363 xhr.send(file); 1364 }, 1365 _onComplete: function(id, xhr){ 1366 // the request was aborted/cancelled 1367 if (!this._files[id]) return; 1368 1369 var name = this.getName(id); 1370 var size = this.getSize(id); 1371 1372 this._options.onProgress(id, name, size, size); 1373 1374 if (xhr.status == 200){ 1375 this.log("xhr - server response received"); 1376 this.log("responseText = " + xhr.responseText); 1377 1378 var response; 1379 1380 try { 1381 response = eval("(" + xhr.responseText + ")"); 1382 } catch(err){ 1383 response = {}; 1384 } 1385 1386 this._options.onComplete(id, name, response); 1387 1388 } else { 1389 this._options.onError(id, name, xhr); 1390 this._options.onComplete(id, name, {}); 1391 } 1392 1393 this._files[id] = null; 1394 this._xhrs[id] = null; 1395 this._dequeue(id); 1396 }, 1397 _cancel: function(id){ 1398 this._options.onCancel(id, this.getName(id)); 1399 1400 this._files[id] = null; 1401 1402 if (this._xhrs[id]){ 1403 this._xhrs[id].abort(); 1404 this._xhrs[id] = null; 1405 } 1406 } 1407}); 1408 1409/** 1410 * A generic module which supports object disposing in dispose() method. 1411 * */ 1412qq.DisposeSupport = { 1413 _disposers: [], 1414 1415 /** Run all registered disposers */ 1416 dispose: function() { 1417 var disposer; 1418 while (disposer = this._disposers.shift()) { 1419 disposer(); 1420 } 1421 }, 1422 1423 /** Add disposer to the collection */ 1424 addDisposer: function(disposeFunction) { 1425 this._disposers.push(disposeFunction); 1426 }, 1427 1428 /** Attach event handler and register de-attacher as a disposer */ 1429 _attach: function() { 1430 this.addDisposer(qq.attach.apply(this, arguments)); 1431 } 1432};
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.