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 abortOnFailure: true, // Fail all files if one doesn't meet the criteria 284 // events 285 // return false to cancel submit 286 onSubmit: function(id, fileName){}, 287 onProgress: function(id, fileName, loaded, total){}, 288 onComplete: function(id, fileName, responseJSON){},
289 onCancel: function(id, fileName){}, 290 onUpload: function(id, fileName, xhr){}, 291 onError: function(id, fileName, xhr) {}, 292 // messages 293 messages: { 294 typeError: "Seuls les types de fichiers {extensions} sont autorisés.", 295 sizeError: "{file} est trop gros, la taille maximale est de {sizeLimit}.", 296 minSizeError: "{file} is too small, minimum file size is {minSizeLimit}.", 297 emptyError: "{file} est vide, veuillez sélectionner d'autres fichiers.", 298 onLeave: "Des fichiers sont en cours de téléchargement. Si vous quittez la page maintenant, le transfert va être annulé." 299 }, 300 showMessage: function(message){ 301 alert(message); 302 }, 303 inputName: 'qqfile', 304 extraDropzones : [] 305 }; 306 qq.extend(this._options, o); 307 qq.extend(this, qq.DisposeSupport); 308 309 // number of files being uploaded 310 this._filesInProgress = 0; 311 this._handler = this._createUploadHandler(); 312 313 if (this._options.button){ 314 this._button = this._createUploadButton(this._options.button); 315 } 316 317 this._preventLeaveInProgress(); 318}; 319 320qq.FileUploaderBasic.prototype = { 321 setParams: function(params){ 322 this._options.params = params; 323 }, 324 getInProgress: function(){ 325 return this._filesInProgress; 326 }, 327 _createUploadButton: function(element){ 328 var self = this; 329 330 var button = new qq.UploadButton({ 331 element: element, 332 multiple: this._options.multiple && qq.UploadHandlerXhr.isSupported(), 333 acceptFiles: this._options.acceptFiles, 334 onChange: function(input){ 335 self._onInputChange(input); 336 } 337 }); 338 339 this.addDisposer(function() { button.dispose(); }); 340 return button; 341 }, 342 _createUploadHandler: function(){ 343 var self = this, 344 handlerClass; 345 346 if(qq.UploadHandlerXhr.isSupported()){ 347 handlerClass = 'UploadHandlerXhr'; 348 } else { 349 handlerClass = 'UploadHandlerForm'; 350 } 351 352 var handler = new qq[handlerClass]({ 353 debug: this._options.debug, 354 action: this._options.action, 355 encoding: this._options.encoding, 356 maxConnections: this._options.maxConnections, 357 customHeaders: this._options.customHeaders, 358 inputName: this._options.inputName, 359 extraDropzones: this._options.extraDropzones, 360 onProgress: function(id, fileName, loaded, total){ 361 self._onProgress(id, fileName, loaded, total); 362 self._options.onProgress(id, fileName, loaded, total); 363 }, 364 onComplete: function(id, fileName, result){ 365 self._onComplete(id, fileName, result); 366 self._options.onComplete(id, fileName, result); 367 }, 368 onCancel: function(id, fileName){ 369 self._onCancel(id, fileName); 370 self._options.onCancel(id, fileName); 371 }, 372 onError: self._options.onError, 373 onUpload: function(id, fileName, xhr){ 374 self._onUpload(id, fileName, xhr); 375 self._options.onUpload(id, fileName, xhr); 376 } 377 }); 378 379 return handler; 380 }, 381 _preventLeaveInProgress: function(){ 382 var self = this; 383 384 this._attach(window, 'beforeunload', function(e){ 385 if (!self._filesInProgress){return;} 386 387 var e = e || window.event; 388 // for ie, ff 389 e.returnValue = self._options.messages.onLeave; 390 // for webkit 391 return self._options.messages.onLeave; 392 }); 393 }, 394 _onSubmit: function(id, fileName){ 395 this._filesInProgress++; 396 }, 397 _onProgress: function(id, fileName, loaded, total){ 398 }, 399 _onComplete: function(id, fileName, result){ 400 this._filesInProgress--; 401 if (result.error){ 402 this._options.showMessage(result.error); 403 } 404 }, 405 _onCancel: function(id, fileName){ 406 this._filesInProgress--; 407 }, 408 _onUpload: function(id, fileName, xhr){ 409 }, 410 _onInputChange: function(input){ 411 if (this._handler instanceof qq.UploadHandlerXhr){ 412 this._uploadFileList(input.files); 413 } else {
414 if (this._validateFile(input)){ 415 this._uploadFile(input); 416 } 417 } 418 this._button.reset(); 419 }, 420 _uploadFileList: function(files){ 421 var goodFiles = []; 422 for (var i=0; i<files.length; i++){ 423 if (this._validateFile(files[i])){ 424 goodFiles.push(files[i]); 425 } else { 426 if (this._options.abortOnFailure) return; 427 } 428 } 429 430 for (var i=0; i<goodFiles.length; i++){ 431 this._uploadFile(goodFiles[i]); 432 } 433 }, 434 _uploadFile: function(fileContainer){ 435 var id = this._handler.add(fileContainer); 436 var fileName = this._handler.getName(id); 437 438 if (this._options.onSubmit(id, fileName) !== false){ 439 this._onSubmit(id, fileName); 440 this._handler.upload(id, this._options.params); 441 } 442 }, 443 _validateFile: function(file){ 444 var name, size; 445 446 if (file.value){ 447 // it is a file input 448 // get input value and remove path to normalize 449 name = file.value.replace(/.*(\/|\\)/, ""); 450 } else { 451 // fix missing properties in Safari 4 and firefox 11.0a2 452 name = (file.fileName !== null && file.fileName !== undefined) ? file.fileName : file.name; 453 size = (file.fileSize !== null && file.fileSize !== undefined) ? file.fileSize : file.size; 454 } 455 456 if (! this._isAllowedExtension(name)){ 457 this._error('typeError', name); 458 return false; 459 460 } else if (size === 0){ 461 this._error('emptyError', name); 462 return false; 463 464 } else if (size && this._options.sizeLimit && size > this._options.sizeLimit){ 465 this._error('sizeError', name); 466 return false; 467 468 } else if (size && size < this._options.minSizeLimit){ 469 this._error('minSizeError', name); 470 return false; 471 } 472 473 return true; 474 }, 475 _error: function(code, fileName){ 476 var message = this._options.messages[code]; 477 function r(name, replacement){ message = message.replace(name, replacement); } 478 479 r('{file}', this._formatFileName(fileName)); 480 r('{extensions}', this._options.allowedExtensions.join(', ')); 481 r('{sizeLimit}', this._formatSize(this._options.sizeLimit)); 482 r('{minSizeLimit}', this._formatSize(this._options.minSizeLimit)); 483 484 this._options.showMessage(message); 485 }, 486 _formatFileName: function(name){ 487 if (name.length > 33){ 488 name = name.slice(0, 19) + '...' + name.slice(-13); 489 } 490 return name; 491 }, 492 _isAllowedExtension: function(fileName){ 493 var ext = (-1 !== fileName.indexOf('.')) 494 ? fileName.replace(/.*[.]/, '').toLowerCase() 495 : ''; 496 var allowed = this._options.allowedExtensions; 497 498 if (!allowed.length){return true;} 499 500 for (var i=0; i<allowed.length; i++){ 501 if (allowed[i].toLowerCase() == ext){ return true;} 502 } 503 504 return false; 505 }, 506 _formatSize: function(bytes){ 507 var i = -1; 508 do { 509 bytes = bytes / 1024; 510 i++; 511 } while (bytes > 99); 512 513 return Math.max(bytes, 0.1).toFixed(1) + ['kB', 'MB', 'GB', 'TB', 'PB', 'EB'][i]; 514 } 515}; 516 517 518/** 519 * Class that creates upload widget with drag-and-drop and file list 520 * @inherits qq.FileUploaderBasic 521 */ 522qq.FileUploader = function(o){ 523 // call parent constructor 524 qq.FileUploaderBasic.apply(this, arguments); 525 526 // additional options 527 qq.extend(this._options, { 528 element: null, 529 // if set, will be used instead of qq-upload-list in template 530 listElement: null, 531 dragText: 'Drop files here to upload', 532 uploadButtonText: 'Parcourir...', 533 cancelButtonText: 'Annuler', 534 failUploadText: 'Echec du transfert', 535 hideShowDropArea: true, 536 537 template: '<div class="qq-uploader">' + 538 //'<div class="qq-upload-drop-area hidden"><span>{dragText}</span></div>'+ 539 '<div class="qq-upload-drop-area hidden"></div>'+ 540 '<div class="qq-upload-button">{uploadButtonText}</div>' + 541 '<ul class="qq-upload-list"></ul>' + 542 '</div>', 543 544 // template for one item in file list 545 fileTemplate: '<li>' + 546 '<span class="qq-progress-bar"></span>' + 547 '<span class="qq-upload-file"></span>' + 548 '<span class="qq-upload-spinner"></span>' + 549 '<span class="qq-upload-size"></span>' + 550 '<a class="qq-upload-cancel" href="#">{cancelButtonText}</a>' + 551 '<span class="qq-upload-failed-text">{failUploadtext}</span>' + 552 '</li>', 553 554 classes: { 555 // used to get elements from templates 556 button: 'qq-upload-button', 557 drop: 'qq-upload-drop-area', 558 dropActive: 'qq-upload-drop-area-active', 559 dropDisabled: 'qq-upload-drop-area-disabled', 560 list: 'qq-upload-list', 561 progressBar: 'qq-progress-bar', 562 file: 'qq-upload-file', 563 spinner: 'qq-upload-spinner', 564 size: 'qq-upload-size', 565 cancel: 'qq-upload-cancel', 566 567 // added to list item <li> when upload completes 568 // used in css to hide progress spinner 569 success: 'qq-upload-success', 570 fail: 'qq-upload-fail' 571 } 572 }); 573 // overwrite options with user supplied 574 qq.extend(this._options, o); 575 576 // overwrite the upload button text if any 577 // same for the Cancel button and Fail message text 578 //this._options.template = this._options.template.replace(/\{dragText\}/g, this._options.dragText); 579 this._options.template = this._options.template.replace(/\{uploadButtonText\}/g, this._options.uploadButtonText); 580 this._options.fileTemplate = this._options.fileTemplate.replace(/\{
580cancelButtonText\}/g, this._options.cancelButtonText); 581 this._options.fileTemplate = this._options.fileTemplate.replace(/\{failUploadtext\}/g, this._options.failUploadText); 582 583 this._element = this._options.element; 584 this._element.innerHTML = this._options.template; 585 this._listElement = this._options.listElement || this._find(this._element, 'list'); 586 587 this._classes = this._options.classes; 588 589 this._button = this._createUploadButton(this._find(this._element, 'button')); 590 591 this._bindCancelEvent(); 592 this._setupDragDrop(); 593}; 594 595// inherit from Basic Uploader 596qq.extend(qq.FileUploader.prototype, qq.FileUploaderBasic.prototype); 597 598qq.extend(qq.FileUploader.prototype, { 599 addExtraDropzone: function(element){ 600 this._setupExtraDropzone(element); 601 }, 602 removeExtraDropzone: function(element){ 603 var dzs = this._options.extraDropzones; 604 for(var i in dzs) if (dzs[i] === element) return this._options.extraDropzones.splice(i,1); 605 }, 606 _leaving_document_out: function(e){ 607 return ((qq.chrome() || (qq.safari() && qq.windows())) && e.clientX == 0 && e.clientY == 0) // null coords for Chrome and Safari Windows 608 || (qq.firefox() && !e.relatedTarget); // null e.relatedTarget for Firefox 609 }, 610 /** 611 * Gets one of the elements listed in this._options.classes 612 **/ 613 _find: function(parent, type){ 614 var element = qq.getByClass(parent, this._options.classes[type])[0]; 615 if (!element){ 616 throw new Error('element not found ' + type); 617 } 618 619 return element; 620 }, 621 _setupExtraDropzone: function(element){ 622 this._options.extraDropzones.push(element); 623 this._setupDropzone(element); 624 }, 625 _setupDropzone: function(dropArea){ 626 var self = this; 627 628 var dz = new qq.UploadDropZone({ 629 element: dropArea, 630 onEnter: function(e){ 631 qq.addClass(dropArea, self._classes.dropActive); 632 e.stopPropagation(); 633 }, 634 onLeave: function(e){ 635 //e.stopPropagation(); 636 }, 637 onLeaveNotDescendants: function(e){ 638 qq.removeClass(dropArea, self._classes.dropActive); 639 }, 640 onDrop: function(e){ 641 if (self._options.hideShowDropArea) { 642 dropArea.style.display = 'none'; 643 } 644 qq.removeClass(dropArea, self._classes.dropActive); 645 self._uploadFileList(e.dataTransfer.files); 646 } 647 }); 648 649 this.addDisposer(function() { dz.dispose(); }); 650 651 if (this._options.hideShowDropArea) { 652 dropArea.style.display = 'none'; 653 } 654 }, 655 _setupDragDrop: function(){ 656 var dropArea = this._find(this._element, 'drop'); 657 var self = this; 658 this._options.extraDropzones.push(dropArea); 659 660 var dropzones = this._options.extraDropzones; 661 var i; 662 for (i=0; i < dropzones.length; i++){ 663 this._setupDropzone(dropzones[i]); 664 } 665 666 // IE <= 9 does not support the File API used for drag+drop uploads 667 // Any volunteers to enable & test this for IE10? 668 if (!qq.ie()) { 669 this._attach(document, 'dragenter', function(e){ 670 // console.log(); 671 if (!self._isValidFileDrag(e)) return; // now causing error. Need it be here? 672 if (qq.hasClass(dropArea, self._classes.dropDisabled)) return; 673 674 dropArea.style.display = 'block'; 675 for (i=0; i < dropzones.length; i++){ dropzones[i].style.display = 'block'; } 676 677 }); 678 } 679 this._attach(document, 'dragleave', function(e){ 680 var relatedTarget = document.elementFromPoint(e.clientX, e.clientY); 681 // only fire when leaving document out 682 if (self._options.hideShowDropArea && 683 qq.FileUploader.prototype._leaving_document_out(e)) { 684 for (i=0; i < dropzones.length; i++){ dropzones[i].style.display = 'none'; } 685 } 686 }); 687 qq.attach(document, 'drop', function(e){ 688 if (self._options.hideShowDropArea) { 689 for (i=0; i < dropzones.length; i++){ dropzones[i].style.display = 'none'; } 690 } 691 e.preventDefault(); 692 }); 693 }, 694 _onSubmit: function(id, fileName){ 695 qq.FileUploaderBasic.prototype._onSubmit.apply(this, arguments); 696 this._addToList(id, fileName); 697 },
698 // Update the progress bar & percentage as the file is uploaded 699 _onProgress: function(id, fileName, loaded, total){ 700 qq.FileUploaderBasic.prototype._onProgress.apply(this, arguments); 701 702 var item = this._getItemByFileId(id); 703 var size = this._find(item, 'size'); 704 size.style.display = 'inline'; 705 706 var text; 707 var percent = Math.round(loaded / total * 100); 708 709 if (loaded != total) { 710 // If still uploading, display percentage 711 text = percent + '% sur ' + this._formatSize(total); 712 } else { 713 // If complete, just display final size 714 text = this._formatSize(total); 715 } 716 717 // Update progress bar <span> tag 718 this._find(item, 'progressBar').style.width = percent + '%'; 719 720 qq.setText(size, text); 721 }, 722 _onComplete: function(id, fileName, result){ 723 qq.FileUploaderBasic.prototype._onComplete.apply(this, arguments); 724 725 // mark completed 726 var item = this._getItemByFileId(id); 727 qq.remove(this._find(item, 'cancel')); 728 qq.remove(this._find(item, 'spinner')); 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.