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