1/** 2 * http://github.com/Valums-File-Uploader/file-uploader 3 * 4 * Multiple file upload component with progress-bar, drag-and-drop, support for all modern browsers. 5 * 6 * Original version: 1.0 © 2010 Andrew Valums ( andrew(at)valums.com ) 7 * Current Maintainer (2.0+): © 2012, Ray Nicholus ( fineuploader(at)garstasio.com ) 8 * 9 * Licensed under MIT license, GNU GPL 2 or later, GNU LGPL 2 or later, see license.txt. 10 */ 11 12var qq = qq || {}; 13var qq = function(element) { 14 "use strict"; 15 16 return { 17 hide: function() { 18 element.style.display = 'none'; 19 return this; 20 }, 21 22 /** Returns the function which detaches attached event */ 23 attach: function(type, fn) { 24 if (element.addEventListener){ 25 element.addEventListener(type, fn, false); 26 } else if (element.attachEvent){ 27 element.attachEvent('on' + type, fn); 28 } 29 return function() { 30 qq(element).detach(type, fn); 31 }; 32 }, 33 34 detach: function(type, fn) { 35 if (element.removeEventListener){ 36 element.removeEventListener(type, fn, false); 37 } else if (element.attachEvent){ 38 element.detachEvent('on' + type, fn); 39 } 40 return this; 41 }, 42 43 contains: function(descendant) { 44 // compareposition returns false in this case 45 if (element == descendant) { 46 return true; 47 } 48 49 if (element.contains){ 50 return element.contains(descendant); 51 } else { 52 return !!(descendant.compareDocumentPosition(element) & 8); 53 } 54 }, 55 56 /** 57 * Insert this element before elementB. 58 */ 59 insertBefore: function(elementB) { 60 elementB.parentNode.insertBefore(element, elementB); 61 return this; 62 }, 63 64 remove: function() { 65 element.parentNode.removeChild(element); 66 return this; 67 }, 68 69 /** 70 * Sets styles for an element. 71 * Fixes opacity in IE6-8. 72 */ 73 css: function(styles) { 74 if (styles.opacity != null){ 75 if (typeof element.style.opacity != 'string' && typeof(element.filters) != 'undefined'){ 76 styles.filter = 'alpha(opacity=' + Math.round(100 * styles.opacity) + ')'; 77 } 78 } 79 qq.extend(element.style, styles); 80 81 return this; 82 }, 83 84 hasClass: function(name) { 85 var re = new RegExp('(^| )' + name + '( |$)'); 86 return re.test(element.className); 87 }, 88 89 addClass: function(name) { 90 if (!qq(element).hasClass(name)){ 91 element.className += ' ' + name; 92 } 93 return this; 94 }, 95 96 removeClass: function(name) { 97 var re = new RegExp('(^| )' + name + '( |$)'); 98 element.className = element.className.replace(re, ' ').replace(/^\s+|\s+$/g, ""); 99 return this; 100 }, 101 102 getByClass: function(className) { 103 if (element.querySelectorAll){ 104 return element.querySelectorAll('.' + className); 105 } 106 107 var result = []; 108 var candidates = element.getElementsByTagName("*"); 109 var len = candidates.length; 110 111 for (var i = 0; i < len; i++){ 112 if (qq(candidates[i]).hasClass(className)){ 113 result.push(candidates[i]); 114 } 115 } 116 return result; 117 }, 118 119 children: function() { 120 var children = [], 121 child = element.firstChild; 122 123 while (child){ 124 if (child.nodeType == 1){ 125 children.push(child); 126 } 127 child = child.nextSibling; 128 } 129 130 return children; 131 }, 132 133 setText: function(text) { 134 element.innerText = text; 135 element.textContent = text; 136 return this; 137 }, 138 139 clearText: function() { 140 return qq(element).setText(""); 141 } 142 }; 143}; 144 145qq.log = function(message, level) { 146 if (window.console) { 147 if (!level || level === 'info') { 148 window.console.log(message); 149 } 150 else 151 { 152 if (window.console[level]) { 153 window.console[level](message); 154 } 155 else { 156 window.console.log('<' + level + '> ' + message); 157 } 158 } 159 } 160}; 161 162qq.isObject = function(variable) { 163 "use strict"; 164 return variable !== null && variable && typeof(variable) === "object" && variable.constructor === Object; 165}; 166 167qq.extend = function (first, second, extendNested) { 168 "use strict"; 169 var prop; 170 for (prop in second) { 171 if (second.hasOwnProperty(prop)) { 172 if (extendNested && qq.isObject(second[prop])) { 173 if (first[prop] === undefined) { 174 first[prop] = {}; 175 } 176 qq.extend(first[prop], second[prop], true); 177 } 178 else { 179 first[prop] = second[prop]; 180 } 181 } 182 } 183}; 184 185/** 186 * Searches for a given element in the array, returns -1 if it is not present. 187 * @param {Number} [from] The index at which to begin the search 188 */ 189qq.indexOf = function(arr, elt, from){ 190 if (arr.indexOf) return arr.indexOf(elt, from); 191 192 from = from || 0; 193 var len = arr.length; 194 195 if (from < 0) from += len; 196 197 for (; from < len; from++){ 198 if (from in arr && arr[from] === elt){ 199 return from; 200 } 201 } 202 return -1; 203}; 204 205qq.getUniqueId = (function(){ 206 var id = 0; 207 return function(){ return id++; }; 208})(); 209 210// 211// Browsers and platforms detection 212 213qq.ie = function(){ return navigator.userAgent.indexOf('MSIE') != -1; } 214qq.ie10 = function(){ return navigator.userAgent.indexOf('MSIE 10') != -1; } 215qq.safari = function(){ return navigator.vendor != undefined && navigator.vendor.indexOf("Apple") != -1; } 216qq.chrome = function(){ return navigator.vendor != undefined && navigator.vendor.indexOf('Google') != -1; } 217qq.firefox = function(){ return (navigator.userAgent.indexOf('Mozilla') != -1 && navigator.vendor != undefined && navigator.vendor == ''); } 218qq.windows = function(){ return navigator.platform == "Win32"; } 219 220// 221// Events 222 223qq.preventDefault = function(e){ 224 if (e.preventDefault){ 225 e.preventDefault(); 226 } else{ 227 e.returnValue = false;
228 } 229}; 230 231/** 232 * Creates and returns element from html string 233 * Uses innerHTML to create an element 234 */ 235qq.toElement = (function(){ 236 var div = document.createElement('div'); 237 return function(html){ 238 div.innerHTML = html; 239 var element = div.firstChild; 240 div.removeChild(element); 241 return element; 242 }; 243})(); 244 245/** 246 * obj2url() takes a json-object as argument and generates 247 * a querystring. pretty much like jQuery.param() 248 * 249 * how to use: 250 * 251 * `qq.obj2url({a:'b',c:'d'},'http://any.url/upload?otherParam=value');` 252 * 253 * will result in: 254 * 255 * `http://any.url/upload?otherParam=value&a=b&c=d` 256 * 257 * @param Object JSON-Object 258 * @param String current querystring-part 259 * @return String encoded querystring 260 */ 261qq.obj2url = function(obj, temp, prefixDone){ 262 var uristrings = [], 263 prefix = '&', 264 add = function(nextObj, i){ 265 var nextTemp = temp 266 ? (/\[\]$/.test(temp)) // prevent double-encoding 267 ? temp 268 : temp+'['+i+']' 269 : i; 270 if ((nextTemp != 'undefined') && (i != 'undefined')) { 271 uristrings.push( 272 (typeof nextObj === 'object') 273 ? qq.obj2url(nextObj, nextTemp, true) 274 : (Object.prototype.toString.call(nextObj) === '[object Function]') 275 ? encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj()) 276 : encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj) 277 ); 278 } 279 }; 280 281 if (!prefixDone && temp) { 282 prefix = (/\?/.test(temp)) ? (/\?$/.test(temp)) ? '' : '&' : '?'; 283 uristrings.push(temp); 284 uristrings.push(qq.obj2url(obj)); 285 } else if ((Object.prototype.toString.call(obj) === '[object Array]') && (typeof obj != 'undefined') ) { 286 // we wont use a for-in-loop on an array (performance) 287 for (var i = 0, len = obj.length; i < len; ++i){ 288 add(obj[i], i); 289 } 290 } else if ((typeof obj != 'undefined') && (obj !== null) && (typeof obj === "object")){ 291 // for anything else but a scalar, we will use for-in-loop 292 for (var i in obj){ 293 add(obj[i], i); 294 } 295 } else { 296 uristrings.push(encodeURIComponent(temp) + '=' + encodeURIComponent(obj)); 297 } 298 299 if (temp) { 300 return uristrings.join(prefix); 301 } else { 302 return uristrings.join(prefix) 303 .replace(/^&/, '') 304 .replace(/%20/g, '+'); 305 } 306}; 307 308/** 309 * A generic module which supports object disposing in dispose() method. 310 * */ 311qq.DisposeSupport = { 312 _disposers: [], 313 314 /** Run all registered disposers */ 315 dispose: function() { 316 var disposer; 317 while (disposer = this._disposers.shift()) { 318 disposer(); 319 } 320 }, 321 322 /** Add disposer to the collection */ 323 addDisposer: function(disposeFunction) { 324 this._disposers.push(disposeFunction); 325 }, 326 327 /** Attach event handler and register de-attacher as a disposer */ 328 _attach: function() { 329 this.addDisposer(qq(arguments[0]).attach.apply(this, Array.prototype.slice.call(arguments, 1))); 330 } 331}; 332qq.UploadButton = function(o){ 333 this._options = { 334 element: null, 335 // if set to true adds multiple attribute to file input 336 multiple: false, 337 acceptFiles: null, 338 // name attribute of file input 339 name: 'file', 340 onChange: function(input){}, 341 hoverClass: 'qq-upload-button-hover', 342 focusClass: 'qq-upload-button-focus' 343 }; 344 345 qq.extend(this._options, o); 346 qq.extend(this, qq.DisposeSupport); 347 348 this._element = this._options.element; 349 350 // make button suitable container for input 351 qq(this._element).css({ 352 position: 'relative', 353 overflow: 'hidden', 354 // Make sure browse button is in the right side 355 // in Internet Explorer 356 direction: 'ltr' 357 }); 358 359 this._input = this._createInput(); 360}; 361 362qq.UploadButton.prototype = { 363 /* returns file input element */ 364 getInput: function(){ 365 return this._input; 366 }, 367 /* cleans/recreates the file input */ 368 reset: function(){ 369 if (this._input.parentNode){ 370 qq(this._input).remove(); 371 } 372 373 qq(this._element).removeClass(this._options.focusClass); 374 this._input = this._createInput(); 375 }, 376 _createInput: function(){ 377 var input = document.createElement("input"); 378 379 if (this._options.multiple){ 380 input.setAttribute("multiple", "multiple"); 381 } 382 383 if (this._options.acceptFiles) input.setAttribute("accept", this._options.acceptFiles); 384 385 input.setAttribute("type", "file"); 386 input.setAttribute("name", this._options.name); 387 388 qq(input).css({ 389 position: 'absolute', 390 // in Opera only 'browse' button 391 // is clickable and it is located at 392 // the right side of the input 393 right: 0, 394 top: 0, 395 fontFamily: 'Arial', 396 // 4 persons reported this, the max values that worked for them were 243, 236, 236, 118 397 fontSize: '118px', 398 margin: 0, 399 padding: 0, 400 cursor: 'pointer', 401 opacity: 0 402 }); 403 404 this._element.appendChild(input); 405 406 var self = this; 407 this._attach(input, 'change', function(){ 408 self._options.onChange(input); 409 }); 410 411 this._attach(input, 'mouseover', function(){ 412 qq(self._element).addClass(self._options.hoverClass); 413 }); 414 this._attach(input, 'mouseout', function(){ 415 qq(self._element).removeClass(self._options.hoverClass); 416 }); 417 this._attach(input, 'focus', function(){ 418 qq(self._element).addClass(self._options.focusClass); 419 }); 420 this._attach(input, 'blur', function(){ 421 qq(self._element).removeClass(self._options.focusClass); 422 }); 423 424 // IE and Opera, unfortunately have 2 tab stops on file input
425 // which is unacceptable in our case, disable keyboard access 426 if (window.attachEvent){ 427 // it is IE or Opera 428 input.setAttribute('tabIndex', "-1"); 429 } 430 431 return input; 432 } 433}; 434qq.FineUploaderBasic = function(o){ 435 var that = this; 436 this._options = { 437 debug: false, 438 button: null, 439 multiple: true, 440 maxConnections: 3, 441 disableCancelForFormUploads: false, 442 autoUpload: true, 443 request: { 444 endpoint: '/server/upload', 445 params: {}, 446 customHeaders: {}, 447 forceMultipart: false, 448 inputName: 'qqfile' 449 }, 450 validation: { 451 allowedExtensions: [], 452 sizeLimit: 0, 453 minSizeLimit: 0, 454 stopOnFirstInvalidFile: true 455 }, 456 callbacks: { 457 onSubmit: function(id, fileName){}, // return false to cancel submit 458 onComplete: function(id, fileName, responseJSON){}, 459 onCancel: function(id, fileName){}, 460 onUpload: function(id, fileName, xhr){}, 461 onProgress: function(id, fileName, loaded, total){}, 462 onError: function(id, fileName, reason) {}, 463 onAutoRetry: function(id, fileName, attemptNumber) {}, 464 onManualRetry: function(id, fileName) {}, 465 onValidate: function(fileData) {} // return false to prevent upload 466 }, 467 messages: { 468 typeError: "{file} has an invalid extension. Valid extension(s): {extensions}.", 469 sizeError: "{file} is too large, maximum file size is {sizeLimit}.", 470 minSizeError: "{file} is too small, minimum file size is {minSizeLimit}.", 471 emptyError: "{file} is empty, please select files again without it.", 472 noFilesError: "No files to upload.", 473 onLeave: "The files are being uploaded, if you leave now the upload will be cancelled." 474 }, 475 retry: { 476 enableAuto: false, 477 maxAutoAttempts: 3, 478 autoAttemptDelay: 5, 479 preventRetryResponseProperty: 'preventRetry' 480 } 481 }; 482 483 qq.extend(this._options, o, true); 484 this._wrapCallbacks(); 485 qq.extend(this, qq.DisposeSupport); 486 487 // number of files being uploaded 488 this._filesInProgress = 0; 489 490 this._storedFileIds = []; 491 492 this._autoRetries = []; 493 this._retryTimeouts = []; 494 this._preventRetries = []; 495 496 this._handler = this._createUploadHandler(); 497 498 if (this._options.button){ 499 this._button = this._createUploadButton(this._options.button); 500 } 501 502 this._preventLeaveInProgress(); 503}; 504 505qq.FineUploaderBasic.prototype = { 506 log: function(str, level) { 507 if (this._options.debug && (!level || level === 'info')) { 508 qq.log('[FineUploader] ' + str); 509 } 510 else if (level && level !== 'info') { 511 qq.log('[FineUploader] ' + str, level); 512 513 } 514 }, 515 setParams: function(params){ 516 this._options.request.params = params; 517 }, 518 getInProgress: function(){ 519 return this._filesInProgress; 520 }, 521 uploadStoredFiles: function(){ 522 "use strict"; 523 while(this._storedFileIds.length) { 524 this._filesInProgress++; 525 this._handler.upload(this._storedFileIds.shift(), this._options.request.params); 526 } 527 }, 528 clearStoredFiles: function(){ 529 this._storedFileIds = []; 530 }, 531 retry: function(id) { 532 if (this._onBeforeManualRetry(id)) { 533 this._handler.retry(id); 534 return true; 535 } 536 else { 537 return false; 538 } 539 }, 540 cancel: function(fileId) { 541 this._handler.cancel(fileId); 542 }, 543 reset: function() { 544 this.log("Resetting uploader..."); 545 this._handler.reset(); 546 this._filesInProgress = 0; 547 this._storedFileIds = []; 548 this._autoRetries = []; 549 this._retryTimeouts = []; 550 this._preventRetries = []; 551 this._button.reset(); 552 }, 553 _createUploadButton: function(element){ 554 var self = this; 555 556 var button = new qq.UploadButton({ 557 element: element, 558 multiple: this._options.multiple && qq.UploadHandlerXhr.isSupported(), 559 acceptFiles: this._options.validation.acceptFiles, 560 onChange: function(input){ 561 self._onInputChange(input); 562 } 563 }); 564 565 this.addDisposer(function() { button.dispose(); }); 566 return button; 567 }, 568 _createUploadHandler: function(){ 569 var self = this, 570 handlerClass; 571 572 if(qq.UploadHandlerXhr.isSupported()){ 573 handlerClass = 'UploadHandlerXhr'; 574 } else { 575 handlerClass = 'UploadHandlerForm';
576 } 577 578 var handler = new qq[handlerClass]({ 579 debug: this._options.debug, 580 endpoint: this._options.request.endpoint, 581 forceMultipart: this._options.request.forceMultipart, 582 maxConnections: this._options.maxConnections, 583 customHeaders: this._options.request.customHeaders, 584 inputName: this._options.request.inputName, 585 demoMode: this._options.demoMode, 586 log: this.log, 587 onProgress: function(id, fileName, loaded, total){ 588 self._onProgress(id, fileName, loaded, total); 589 self._options.callbacks.onProgress(id, fileName, loaded, total); 590 }, 591 onComplete: function(id, fileName, result, xhr){ 592 self._onComplete(id, fileName, result, xhr); 593 self._options.callbacks.onComplete(id, fileName, result); 594 }, 595 onCancel: function(id, fileName){ 596 self._onCancel(id, fileName); 597 self._options.callbacks.onCancel(id, fileName); 598 }, 599 onUpload: function(id, fileName, xhr){ 600 self._onUpload(id, fileName, xhr); 601 self._options.callbacks.onUpload(id, fileName, xhr); 602 }, 603 onAutoRetry: function(id, fileName, responseJSON, xhr) { 604 self._preventRetries[id] = responseJSON[self._options.retry.preventRetryResponseProperty]; 605 606 if (self._shouldAutoRetry(id, fileName, responseJSON)) { 607 self._maybeParseAndSendUploadError(id, fileName, responseJSON, xhr); 608 self._options.callbacks.onAutoRetry(id, fileName, self._autoRetries[id] + 1); 609 self._onBeforeAutoRetry(id, fileName); 610 611 self._retryTimeouts[id] = setTimeout(function() { 612 self._onAutoRetry(id, fileName, responseJSON) 613 }, self._options.retry.autoAttemptDelay * 1000); 614 615 return true; 616 } 617 else { 618 return false; 619 } 620 } 621 }); 622 623 return handler; 624 }, 625 _preventLeaveInProgress: function(){ 626 var self = this; 627 628 this._attach(window, 'beforeunload', function(e){ 629 if (!self._filesInProgress){return;} 630 631 var e = e || window.event; 632 // for ie, ff 633 e.returnValue = self._options.messages.onLeave; 634 // for webkit 635 return self._options.messages.onLeave; 636 }); 637 }, 638 _onSubmit: function(id, fileName){ 639 if (this._options.autoUpload) { 640 this._filesInProgress++; 641 } 642 }, 643 _onProgress: function(id, fileName, loaded, total){ 644 }, 645 _onComplete: function(id, fileName, result, xhr){ 646 this._filesInProgress--; 647 this._maybeParseAndSendUploadError(id, fileName, result, xhr); 648 }, 649 _onCancel: function(id, fileName){ 650 clearTimeout(this._retryTimeouts[id]); 651 652 var storedFileIndex = qq.indexOf(this._storedFileIds, id); 653 if (this._options.autoUpload || storedFileIndex < 0) { 654 this._filesInProgress--; 655 } 656 else if (!this._options.autoUpload) { 657 this._storedFileIds.splice(storedFileIndex, 1); 658 } 659 }, 660 _onUpload: function(id, fileName, xhr){ 661 }, 662 _onInputChange: function(input){ 663 if (this._handler instanceof qq.UploadHandlerXhr){ 664 this._uploadFileList(input.files); 665 } else { 666 if (this._validateFile(input)){ 667 this._uploadFile(input); 668 } 669 } 670 this._button.reset(); 671 }, 672 _onBeforeAutoRetry: function(id, fileName) { 673 this.log("Waiting " + this._options.retry.autoAttemptDelay + " seconds before retrying " + fileName + "..."); 674 }, 675 _onAutoRetry: function(id, fileName, responseJSON) { 676 this.log("Retrying " + fileName + "..."); 677 this._autoRetries[id]++; 678 this._handler.retry(id); 679 }, 680 _shouldAutoRetry: function(id, fileName, responseJSON) { 681 if (!this._preventRetries[id] && this._options.retry.enableAuto) { 682 if (this._autoRetries[id] === undefined) { 683 this._autoRetries[id] = 0; 684 } 685 686 return this._autoRetries[id] < this._options.retry.maxAutoAttempts 687 } 688 689 return false;
690 }, 691 //return false if we should not attempt the requested retry 692 _onBeforeManualRetry: function(id) { 693 if (this._preventRetries[id]) { 694 this.log("Retries are forbidden for id " + id, 'warn'); 695 return false; 696 } 697 else if (this._handler.isValid(id)) { 698 var fileName = this._handler.getName(id); 699 700 if (this._options.callbacks.onManualRetry(id, fileName) === false) { 701 return false; 702 } 703 704 this.log("Retrying upload for '" + fileName + "' (id: " + id + ")..."); 705 this._filesInProgress++; 706 return true; 707 } 708 else { 709 this.log("'" + id + "' is not a valid file ID", 'error'); 710 return false; 711 } 712 }, 713 _maybeParseAndSendUploadError: function(id, fileName, response, xhr) { 714 //assuming no one will actually set the response code to something other than 200 and still set 'success' to true 715 if (!response.success){ 716 if (xhr && xhr.status !== 200 && !response.error) { 717 this._options.callbacks.onError(id, fileName, "XHR returned response code " + xhr.status); 718 } 719 else { 720 var errorReason = response.error ? response.error : "Upload failure reason unknown"; 721 this._options.callbacks.onError(id, fileName, errorReason); 722 } 723 } 724 }, 725 _uploadFileList: function(files){ 726 var validationDescriptors, index, batchInvalid; 727 728 validationDescriptors = this._getValidationDescriptors(files); 729 if (validationDescriptors.length > 1) { 730 batchInvalid = this._options.callbacks.onValidate(validationDescriptors) === false; 731 } 732 733 if (!batchInvalid) { 734 if (files.length > 0) { 735 for (index = 0; index < files.length; index++){ 736 if (this._validateFile(files[index])){ 737 this._uploadFile(files[index]); 738 } else { 739 if (this._options.validation.stopOnFirstInvalidFile){ 740 return; 741 } 742 } 743 } 744 } 745 else { 746 this._error('noFilesError', ""); 747 } 748 } 749 }, 750 _uploadFile: function(fileContainer){ 751 var id = this._handler.add(fileContainer); 752 var fileName = this._handler.getName(id); 753 754 if (this._options.callbacks.onSubmit(id, fileName) !== false){ 755 this._onSubmit(id, fileName); 756 if (this._options.autoUpload) { 757 this._handler.upload(id, this._options.request.params); 758 } 759 else { 760 this._storeFileForLater(id); 761 } 762 } 763 }, 764 _storeFileForLater: function(id) { 765 this._storedFileIds.push(id); 766 }, 767 _validateFile: function(file){ 768 var validationDescriptor, name, size; 769 770 validationDescriptor = this._getValidationDescriptor(file); 771 name = validationDescriptor.name; 772 size = validationDescriptor.size; 773 774 if (this._options.callbacks.onValidate([validationDescriptor]) === false) { 775 return false; 776 } 777 778 if (!this._isAllowedExtension(name)){ 779 this._error('typeError', name); 780 return false; 781 782 } 783 else if (size === 0){ 784 this._error('emptyError', name); 785 return false; 786 787 } 788 else if (size && this._options.validation.sizeLimit && size > this._options.validation.sizeLimit){ 789 this._error('sizeError', name); 790 return false; 791 792 } 793 else if (size && size < this._options.validation.minSizeLimit){ 794 this._error('minSizeError', name); 795 return false; 796 } 797 798 return true; 799 }, 800 _error: function(code, fileName){ 801 var message = this._options.messages[code]; 802 function r(name, replacement){ message = message.replace(name, replacement); } 803 804 var extensions = this._options.validation.allowedExtensions.join(', '); 805 806 r('{file}', this._formatFileName(fileName)); 807 r('{extensions}', extensions); 808 r('{sizeLimit}', this._formatSize(this._options.validation.sizeLimit)); 809 r('{minSizeLimit}', this._formatSize(this._options.validation.minSizeLimit)); 810 811 this._options.callbacks.onError(null, fileName, message); 812 813 return message; 814 }, 815 _formatFileName: function(name){ 816 if (name.length > 33){ 817 name = name.slice(0, 19) + '...' + name.slice(-13); 818 } 819 return name; 820 }, 821 _isAllowedExtension: function(fileName){ 822 var ext = (-1 !== fileName.indexOf('.')) 823 ? fileName.replace(/.*[.]/, '').toLowerCase() 824 : ''; 825 var allowed = this._options.validation.allowedExtensions; 826 827 if (!allowed.length){return true;} 828 829 for (var i=0; i<allowed.length; i++){ 830 if (allowed[i].toLowerCase() == ext){ return true;} 831 } 832 833 return false;
834 }, 835 _formatSize: function(bytes){ 836 var i = -1; 837 do { 838 bytes = bytes / 1024; 839 i++; 840 } while (bytes > 99); 841 842 return Math.max(bytes, 0.1).toFixed(1) + ['kB', 'MB', 'GB', 'TB', 'PB', 'EB'][i]; 843 }, 844 _wrapCallbacks: function() { 845 var self, safeCallback; 846 847 self = this; 848 849 safeCallback = function(name, callback, args) { 850 try { 851 return callback.apply(self, args); 852 } 853 catch (exception) { 854 self.log("Caught exception in '" + name + "' callback - " + exception, 'error'); 855 } 856 } 857 858 for (var prop in this._options.callbacks) { 859 (function() { 860 var oldCallback = self._options.callbacks[prop]; 861 self._options.callbacks[prop] = function() { 862 return safeCallback(prop, oldCallback, arguments); 863 } 864 }()); 865 } 866 }, 867 _parseFileName: function(file) { 868 var name; 869 870 if (file.value){ 871 // it is a file input 872 // get input value and remove path to normalize 873 name = file.value.replace(/.*(\/|\\)/, ""); 874 } else { 875 // fix missing properties in Safari 4 and firefox 11.0a2 876 name = (file.fileName !== null && file.fileName !== undefined) ? file.fileName : file.name; 877 } 878 879 return name; 880 }, 881 _parseFileSize: function(file) { 882 var size; 883 884 if (!file.value){ 885 // fix missing properties in Safari 4 and firefox 11.0a2 886 size = (file.fileSize !== null && file.fileSize !== undefined) ? file.fileSize : file.size; 887 } 888 889 return size; 890 }, 891 _getValidationDescriptor: function(file) { 892 var name, size, fileDescriptor; 893 894 fileDescriptor = {}; 895 name = this._parseFileName(file); 896 size = this._parseFileSize(file); 897 898 fileDescriptor.name = name; 899 if (size) { 900 fileDescriptor.size = size; 901 } 902 903 return fileDescriptor; 904 }, 905 _getValidationDescriptors: function(files) { 906 var index, fileDescriptors; 907 908 fileDescriptors = []; 909 910 for (index = 0; index < files.length; index++) { 911 fileDescriptors.push(files[index]); 912 } 913 914 return fileDescriptors; 915 } 916}; 917/** 918 * Class that creates upload widget with drag-and-drop and file list 919 * @inherits qq.FineUploaderBasic 920 */ 921qq.FineUploader = function(o){ 922 // call parent constructor 923 qq.FineUploaderBasic.apply(this, arguments); 924 925 // additional options 926 qq.extend(this._options, { 927 element: null, 928 listElement: null, 929 dragAndDrop: { 930 extraDropzones: [], 931 hideDropzones: true, 932 disableDefaultDropzone: false 933 }, 934 text: { 935 uploadButton: 'Upload a file', 936 cancelButton: 'Cancel', 937 retryButton: 'Retry', 938 failUpload: 'Upload failed', 939 dragZone: 'Drop files here to upload', 940 formatProgress: "{percent}% of {total_size}", 941 waitingForResponse: "Processing..." 942 }, 943 template: '<div class="qq-uploader">' + 944 ((!this._options.dragAndDrop || !this._options.dragAndDrop.disableDefaultDropzone) ? '<div class="qq-upload-drop-area"><span>{dragZoneText}</span></div>' : '') + 945 (!this._options.button ? '<div class="qq-upload-button"><div>{uploadButtonText}</div></div>' : '') + 946 (!this._options.listElement ? '<ul class="qq-upload-list"></ul>' : '') + 947 '</div>', 948 949 // template for one item in file list 950 fileTemplate: '<li>' + 951 '<div class="qq-progress-bar"></div>' + 952 '<span class="qq-upload-spinner"></span>' + 953 '<span class="qq-upload-finished"></span>' + 954 '<span class="qq-upload-file"></span>' + 955 '<span class="qq-upload-size"></span>' + 956 '<a class="qq-upload-cancel" href="#">{cancelButtonText}</a>' + 957 '<a class="qq-upload-retry" href="#">{retryButtonText}</a>' + 958 '<span class="qq-upload-status-text">{statusText}</span>' + 959 '</li>', 960 classes: { 961 // used to get elements from templates 962 button: 'qq-upload-button', 963 drop: 'qq-upload-drop-area', 964 dropActive: 'qq-upload-drop-area-active', 965 dropDisabled: 'qq-upload-drop-area-disabled', 966 list: 'qq-upload-list', 967 progressBar: 'qq-progress-bar', 968 file: 'qq-upload-file', 969 spinner: 'qq-upload-spinner', 970 finished: 'qq-upload-finished', 971 retrying: 'qq-upload-retrying', 972 retryable: 'qq-upload-retryable', 973 size: 'qq-upload-size', 974 cancel: 'qq-upload-cancel', 975 retry: 'qq-upload-retry', 976 statusText: 'qq-upload-status-text', 977 978 // added to list item <li> when upload completes 979 // used in css to hide progress spinner 980 success: 'qq-upload-success', 981 fail: 'qq-upload-fail', 982 983 successIcon: null, 984 failIcon: null 985 }, 986 failedUploadTextDisplay: { 987 mode: 'default', //default, custom, or none 988 maxChars: 50, 989 responseProperty: 'error', 990 enableTooltip: true 991 }, 992 messages: { 993 tooManyFilesError: "You may only drop one file" 994 }, 995 retry: { 996 showAutoRetryNote: true, 997 autoRetryNote: "Retrying {retryNum}/{maxAuto}...",
998 showButton: false 999 }, 1000 showMessage: function(message){ 1001 alert(message); 1002 } 1003 }, true); 1004 1005 // overwrite options with user supplied 1006 qq.extend(this._options, o, true); 1007 this._wrapCallbacks(); 1008 1009 // overwrite the upload button text if any 1010 // same for the Cancel button and Fail message text 1011 this._options.template = this._options.template.replace(/\{dragZoneText\}/g, this._options.text.dragZone); 1012 this._options.template = this._options.template.replace(/\{uploadButtonText\}/g, this._options.text.uploadButton); 1013 this._options.fileTemplate = this._options.fileTemplate.replace(/\{cancelButtonText\}/g, this._options.text.cancelButton); 1014 this._options.fileTemplate = this._options.fileTemplate.replace(/\{retryButtonText\}/g, this._options.text.retryButton); 1015 this._options.fileTemplate = this._options.fileTemplate.replace(/\{statusText\}/g, ""); 1016 1017 this._element = this._options.element; 1018 this._element.innerHTML = this._options.template; 1019 this._listElement = this._options.listElement || this._find(this._element, 'list'); 1020 1021 this._classes = this._options.classes; 1022 1023 if (!this._button) { 1024 this._button = this._createUploadButton(this._find(this._element, 'button')); 1025 } 1026 1027 this._bindCancelAndRetryEvents(); 1028 this._setupDragDrop(); 1029}; 1030 1031// inherit from Basic Uploader 1032qq.extend(qq.FineUploader.prototype, qq.FineUploaderBasic.prototype); 1033 1034qq.extend(qq.FineUploader.prototype, { 1035 clearStoredFiles: function() { 1036 qq.FineUploaderBasic.prototype.clearStoredFiles.apply(this, arguments); 1037 this._listElement.innerHTML = ""; 1038 }, 1039 addExtraDropzone: function(element){ 1040 this._setupExtraDropzone(element); 1041 }, 1042 removeExtraDropzone: function(element){ 1043 var dzs = this._options.dragAndDrop.extraDropzones; 1044 for(var i in dzs) if (dzs[i] === element) return this._options.dragAndDrop.extraDropzones.splice(i,1); 1045 }, 1046 getItemByFileId: function(id){ 1047 var item = this._listElement.firstChild; 1048 1049 // there can't be txt nodes in dynamically created list 1050 // and we can use nextSibling 1051 while (item){ 1052 if (item.qqFileId == id) return item; 1053 item = item.nextSibling; 1054 } 1055 }, 1056 reset: function() { 1057 qq.FineUploaderBasic.prototype.reset.apply(this, arguments); 1058 this._element.innerHTML = this._options.template; 1059 this._listElement = this._options.listElement || this._find(this._element, 'list'); 1060 if (!this._options.button) { 1061 this._button = this._createUploadButton(this._find(this._element, 'button')); 1062 } 1063 this._bindCancelAndRetryEvents(); 1064 this._setupDragDrop(); 1065 }, 1066 _leaving_document_out: function(e){ 1067 return ((qq.chrome() || (qq.safari() && qq.windows())) && e.clientX == 0 && e.clientY == 0) // null coords for Chrome and Safari Windows 1068 || (qq.firefox() && !e.relatedTarget); // null e.relatedTarget for Firefox 1069 }, 1070 _storeFileForLater: function(id) { 1071 qq.FineUploaderBasic.prototype._storeFileForLater.apply(this, arguments); 1072 var item = this.getItemByFileId(id); 1073 qq(this._find(item, 'spinner')).hide(); 1074 }, 1075 /** 1076 * Gets one of the elements listed in this._options.classes 1077 **/ 1078 _find: function(parent, type){ 1079 var element = qq(parent).getByClass(this._options.classes[type])[0]; 1080 if (!element){ 1081 throw new Error('element not found ' + type); 1082 } 1083 1084 return element; 1085 }, 1086 _setupExtraDropzone: function(element){ 1087 this._options.dragAndDrop.extraDropzones.push(element); 1088 this._setupDropzone(element); 1089 }, 1090 _setupDropzone: function(dropArea){ 1091 var self = this; 1092 1093 var dz = new qq.UploadDropZone({ 1094 element: dropArea, 1095 onEnter: function(e){ 1096 qq(dropArea).addClass(self._classes.dropActive); 1097 e.stopPropagation(); 1098 }, 1099 onLeave: function(e){ 1100 //e.stopPropagation(); 1101 }, 1102 onLeaveNotDescendants: function(e){ 1103 qq(dropArea).removeClass(self._classes.dropActive); 1104 }, 1105 onDrop: function(e){ 1106 if (self._options.dragAndDrop.hideDropzones) { 1107 qq(dropArea).hide(); 1108 } 1109 1110 qq(dropArea).removeClass(self._classes.dropActive); 1111 if (e.dataTransfer.files.length > 1 && !self._options.multiple) { 1112 self._error('tooManyFilesError', ""); 1113 } 1114 else { 1115 self._uploadFileList(e.dataTransfer.files); 1116 } 1117 } 1118 }); 1119 1120 this.addDisposer(function() { dz.dispose(); }); 1121 1122 if (this._options.dragAndDrop.hideDropzones) { 1123 qq(dropArea).hide(); 1124 } 1125 }, 1126 _setupDragDrop: function(){ 1127 var self, dropArea; 1128 1129 self = this; 1130 1131 if (!this._options.dragAndDrop.disableDefaultDropzone) { 1132 dropArea = this._find(this._element, 'drop'); 1133 this._options.dragAndDrop.extraDropzones.push(dropArea); 1134 } 1135 1136 var dropzones = this._options.dragAndDrop.extraDropzones; 1137 var i; 1138 for (i=0; i < dropzones.length; i++){ 1139 this._setupDropzone(dropzones[i]); 1140 } 1141
1142 // IE <= 9 does not support the File API used for drag+drop uploads 1143 if (!this._options.dragAndDrop.disableDefaultDropzone && (!qq.ie() || qq.ie10())) { 1144 this._attach(document, 'dragenter', function(e){ 1145 if (qq(dropArea).hasClass(self._classes.dropDisabled)) return; 1146 1147 dropArea.style.display = 'block'; 1148 for (i=0; i < dropzones.length; i++){ dropzones[i].style.display = 'block'; } 1149 1150 }); 1151 } 1152 this._attach(document, 'dragleave', function(e){ 1153 if (self._options.dragAndDrop.hideDropzones && qq.FineUploader.prototype._leaving_document_out(e)) { 1154 for (i=0; i < dropzones.length; i++) { 1155 qq(dropzones[i]).hide(); 1156 } 1157 } 1158 }); 1159 qq(document).attach('drop', function(e){ 1160 if (self._options.dragAndDrop.hideDropzones) { 1161 for (i=0; i < dropzones.length; i++) { 1162 qq(dropzones[i]).hide(); 1163 } 1164 } 1165 e.preventDefault(); 1166 }); 1167 }, 1168 _onSubmit: function(id, fileName){ 1169 qq.FineUploaderBasic.prototype._onSubmit.apply(this, arguments); 1170 this._addToList(id, fileName); 1171 }, 1172 // Update the progress bar & percentage as the file is uploaded 1173 _onProgress: function(id, fileName, loaded, total){ 1174 qq.FineUploaderBasic.prototype._onProgress.apply(this, arguments); 1175 1176 var item, progressBar, text, percent, cancelLink, size; 1177 1178 item = this.getItemByFileId(id); 1179 progressBar = this._find(item, 'progressBar'); 1180 percent = Math.round(loaded / total * 100); 1181 1182 if (loaded === total) { 1183 cancelLink = this._find(item, 'cancel'); 1184 qq(cancelLink).hide(); 1185 1186 qq(progressBar).hide(); 1187 qq(this._find(item, 'statusText')).setText(this._options.text.waitingForResponse); 1188 1189 // If last byte was sent, just display final size 1190 text = this._formatSize(total); 1191 } 1192 else { 1193 // If still uploading, display percentage 1194 text = this._formatProgress(loaded, total); 1195 1196 qq(progressBar).css({display: 'block'}); 1197 } 1198 1199 // Update progress bar element 1200 qq(progressBar).css({width: percent + '%'}); 1201 1202 size = this._find(item, 'size'); 1203 qq(size).css({display: 'inline'}); 1204 qq(size).setText(text); 1205 }, 1206 _onComplete: function(id, fileName, result, xhr){ 1207 qq.FineUploaderBasic.prototype._onComplete.apply(this, arguments); 1208 1209 var item = this.getItemByFileId(id); 1210 1211 qq(this._find(item, 'statusText')).clearText(); 1212 1213 qq(item).removeClass(this._classes.retrying); 1214 qq(this._find(item, 'progressBar')).hide(); 1215 1216 if (!this._options.disableCancelForFormUploads || qq.UploadHandlerXhr.isSupported()) { 1217 qq(this._find(item, 'cancel')).hide(); 1218 } 1219 qq(this._find(item, 'spinner')).hide(); 1220 1221 if (result.success){ 1222 qq(item).addClass(this._classes.success); 1223 if (this._classes.successIcon) { 1224 this._find(item, 'finished').style.display = "inline-block"; 1225 qq(item).addClass(this._classes.successIcon); 1226 } 1227 } else { 1228 qq(item).addClass(this._classes.fail); 1229 if (this._classes.failIcon) { 1230 this._find(item, 'finished').style.display = "inline-block"; 1231 qq(item).addClass(this._classes.failIcon); 1232 } 1233 if (this._options.retry.showButton && !this._preventRetries[id]) { 1234 qq(item).addClass(this._classes.retryable); 1235 } 1236 this._controlFailureTextDisplay(item, result); 1237 } 1238 }, 1239 _onUpload: function(id, fileName, xhr){ 1240 qq.FineUploaderBasic.prototype._onUpload.apply(this, arguments); 1241 1242 var item = this.getItemByFileId(id); 1243 this._showSpinner(item); 1244 }, 1245 _onBeforeAutoRetry: function(id) { 1246 var item, progressBar, cancelLink, failTextEl, retryNumForDisplay, maxAuto, retryNote; 1247 1248 qq.FineUploaderBasic.prototype._onBeforeAutoRetry.apply(this, arguments); 1249 1250 item = this.getItemByFileId(id); 1251 progressBar = this._find(item, 'progressBar'); 1252 1253 this._showCancelLink(item); 1254 progressBar.style.width = 0; 1255 qq(progressBar).hide(); 1256 1257 if (this._options.retry.showAutoRetryNote) { 1258 failTextEl = this._find(item, 'statusText'); 1259 retryNumForDisplay = this._autoRetries[id] + 1; 1260 maxAuto = this._options.retry.maxAutoAttempts; 1261 1262 retryNote = this._options.retry.autoRetryNote.replace(/\{retryNum\}/g, retryNumForDisplay); 1263 retryNote = retryNote.replace(/\{maxAuto\}/g, maxAuto); 1264 1265 qq(failTextEl).setText(retryNote); 1266 if (retryNumForDisplay === 1) { 1267 qq(item).addClass(this._classes.retrying); 1268 } 1269 } 1270 }, 1271 //return false if we should not attempt the requested retry 1272 _onBeforeManualRetry: function(id) { 1273 if (qq.FineUploaderBasic.prototype._onBeforeManualRetry.apply(this, arguments)) { 1274 var item = this.getItemByFileId(id); 1275 this._find(item, 'progressBar').style.width = 0; 1276 qq(item).removeClass(this._classes.fail); 1277 this._showSpinner(item); 1278 this._showCancelLink(item); 1279 return true; 1280 } 1281 return false;
1282 }, 1283 _addToList: function(id, fileName){ 1284 var item = qq.toElement(this._options.fileTemplate); 1285 if (this._options.disableCancelForFormUploads && !qq.UploadHandlerXhr.isSupported()) { 1286 var cancelLink = this._find(item, 'cancel'); 1287 qq(cancelLink).remove(); 1288 } 1289 1290 item.qqFileId = id; 1291 1292 var fileElement = this._find(item, 'file'); 1293 qq(fileElement).setText(this._formatFileName(fileName)); 1294 qq(this._find(item, 'size')).hide(); 1295 if (!this._options.multiple) this._clearList(); 1296 this._listElement.appendChild(item); 1297 }, 1298 _clearList: function(){ 1299 this._listElement.innerHTML = ''; 1300 this.clearStoredFiles(); 1301 }, 1302 /** 1303 * delegate click event for cancel & retry links 1304 **/ 1305 _bindCancelAndRetryEvents: function(){ 1306 var self = this, 1307 list = this._listElement; 1308 1309 this._attach(list, 'click', function(e){ 1310 e = e || window.event; 1311 var target = e.target || e.srcElement; 1312 1313 if (qq(target).hasClass(self._classes.cancel) || qq(target).hasClass(self._classes.retry)){ 1314 qq.preventDefault(e); 1315 1316 var item = target.parentNode; 1317 while(item.qqFileId == undefined) { 1318 item = target = target.parentNode; 1319 } 1320 1321 if (qq(target).hasClass(self._classes.cancel)) { 1322 self.cancel(item.qqFileId); 1323 qq(item).remove(); 1324 } 1325 else { 1326 qq(item).removeClass(self._classes.retryable); 1327 self.retry(item.qqFileId); 1328 } 1329 } 1330 }); 1331 }, 1332 _formatProgress: function (uploadedSize, totalSize) { 1333 var message = this._options.text.formatProgress; 1334 function r(name, replacement) { message = message.replace(name, replacement); } 1335 1336 r('{percent}', Math.round(uploadedSize / totalSize * 100)); 1337 r('{total_size}', this._formatSize(totalSize)); 1338 return message; 1339 }, 1340 _controlFailureTextDisplay: function(item, response) { 1341 var mode, maxChars, responseProperty, failureReason, shortFailureReason; 1342 1343 mode = this._options.failedUploadTextDisplay.mode; 1344 maxChars = this._options.failedUploadTextDisplay.maxChars; 1345 responseProperty = this._options.failedUploadTextDisplay.responseProperty; 1346 1347 if (mode === 'custom') { 1348 failureReason = response[responseProperty]; 1349 if (failureReason) { 1350 if (failureReason.length > maxChars) { 1351 shortFailureReason = failureReason.substring(0, maxChars) + '...'; 1352 } 1353 } 1354 else { 1355 failureReason = this._options.text.failUpload; 1356 this.log("'" + responseProperty + "' is not a valid property on the server response.", 'w
1356arn'); 1357 } 1358 1359 qq(this._find(item, 'statusText')).setText(shortFailureReason || failureReason); 1360 1361 if (this._options.failedUploadTextDisplay.enableTooltip) { 1362 this._showTooltip(item, failureReason); 1363 } 1364 } 1365 else if (mode === 'default') { 1366 qq(this._find(item, 'statusText')).setText(this._options.text.failUpload); 1367 } 1368 else if (mode !== 'none') { 1369 this.log("failedUploadTextDisplay.mode value of '" + mode + "' is not valid", 'warn'); 1370 } 1371 }, 1372 //TODO turn this into a real tooltip, with click trigger (so it is usable on mobile devices). See case #355 for details. 1373 _showTooltip: function(item, text) { 1374 item.title = text; 1375 }, 1376 _showSpinner: function(item) { 1377 var spinnerEl = this._find(item, 'spinner'); 1378 spinnerEl.style.display = "inline-block"; 1379 }, 1380 _showCancelLink: function(item) { 1381 if (!this._options.disableCancelForFormUploads || qq.UploadHandlerXhr.isSupported()) { 1382 var cancelLink = this._find(item, 'cancel'); 1383 cancelLink.style.display = 'inline'; 1384 } 1385 }, 1386 _error: function(code, fileName){ 1387 var message = qq.FineUploaderBasic.prototype._error.apply(this, arguments); 1388 this._options.showMessage(message); 1389 } 1390}); 1391 1392qq.UploadDropZone = function(o){ 1393 this._options = { 1394 element: null, 1395 onEnter: function(e){}, 1396 onLeave: function(e){}, 1397 // is not fired when leaving element by hovering descendants 1398 onLeaveNotDescendants: function(e){}, 1399 onDrop: function(e){} 1400 }; 1401 qq.extend(this._options, o); 1402 qq.extend(this, qq.DisposeSupport); 1403 1404 this._element = this._options.element; 1405 1406 this._disableDropOutside(); 1407 this._attachEvents(); 1408}; 1409 1410qq.UploadDropZone.prototype = { 1411 _dragover_should_be_canceled: function(){ 1412 return qq.safari() || (qq.firefox() && qq.windows()); 1413 }, 1414 _disableDropOutside: function(e){ 1415 // run only once for all instances 1416 if (!qq.UploadDropZone.dropOutsideDisabled ){ 1417 1418 // for these cases we need to catch onDrop to reset dropArea 1419 if (this._dragover_should_be_canceled){ 1420 qq(document).attach('dragover', function(e){ 1421 e.preventDefault(); 1422 }); 1423 } else { 1424 qq(document).attach('dragover', function(e){ 1425 if (e.dataTransfer){ 1426 e.dataTransfer.dropEffect = 'none'; 1427 e.preventDefault(); 1428 } 1429 }); 1430 } 1431 1432 qq.UploadDropZone.dropOutsideDisabled = true; 1433 } 1434 }, 1435 _attachEvents: function(){ 1436 var self = this; 1437 1438 self._attach(self._element, 'dragover', function(e){ 1439 if (!self._isValidFileDrag(e)) return; 1440 1441 var effect = qq.ie() ? null : e.dataTransfer.effectAllowed; 1442 if (effect == 'move' || effect == 'linkMove'){ 1443 e.dataTransfer.dropEffect = 'move'; // for FF (only move allowed) 1444 } else { 1445 e.dataTransfer.dropEffect = 'copy'; // for Chrome 1446 } 1447 1448 e.stopPropagation(); 1449 e.preventDefault(); 1450 }); 1451 1452 self._attach(self._element, 'dragenter', function(e){ 1453 if (!self._isValidFileDrag(e)) return; 1454 1455 self._options.onEnter(e); 1456 }); 1457 1458 self._attach(self._element, 'dragleave', function(e){ 1459 if (!self._isValidFileDrag(e)) return; 1460 1461 self._options.onLeave(e); 1462 1463 var relatedTarget = document.elementFromPoint(e.clientX, e.clientY); 1464 // do not fire when moving a mouse over a descendant 1465 if (qq(this).contains(relatedTarget)) return; 1466 1467 self._options.onLeaveNotDescendants(e); 1468 }); 1469 1470 self._attach(self._element, 'drop', function(e){ 1471 if (!self._isValidFileDrag(e)) return; 1472 1473 e.preventDefault(); 1474 self._options.onDrop(e); 1475 }); 1476 }, 1477 _isValidFileDrag: function(e){ 1478 // e.dataTransfer currently causing IE errors 1479 // IE9 does NOT support file API, so drag-and-drop is not possible 1480 if (qq.ie() && !qq.ie10()) return false;
1481 1482 var dt = e.dataTransfer, 1483 // do not check dt.types.contains in webkit, because it crashes safari 4 1484 isSafari = qq.safari(); 1485 1486 // dt.effectAllowed is none in Safari 5 1487 // dt.types.contains check is for firefox 1488 var effectTest = qq.ie10() ? true : dt.effectAllowed != 'none'; 1489 return dt && effectTest && (dt.files || (!isSafari && dt.types.contains && dt.types.contains('Files'))); 1490 } 1491}; 1492/** 1493 * Class for uploading files, uploading itself is handled by child classes 1494 */ 1495qq.UploadHandlerAbstract = function(o){ 1496 // Default options, can be overridden by the user 1497 this._options = { 1498 debug: false, 1499 endpoint: '/upload.php', 1500 // maximum number of concurrent uploads 1501 maxConnections: 999, 1502 log: function(str, level) {}, 1503 onProgress: function(id, fileName, loaded, total){}, 1504 onComplete: function(id, fileName, response, xhr){}, 1505 onCancel: function(id, fileName){}, 1506 onUpload: function(id, fileName, xhr){}, 1507 onAutoRetry: function(id, fileName, response, xhr){} 1508 1509 }; 1510 qq.extend(this._options, o); 1511 1512 this._queue = []; 1513 // params for files in queue 1514 this._params = []; 1515 1516 this.log = this._options.log; 1517}; 1518qq.UploadHandlerAbstract.prototype = { 1519 /** 1520 * Adds file or file input to the queue 1521 * @returns id 1522 **/ 1523 add: function(file){}, 1524 /** 1525 * Sends the file identified by id and additional query params to the server 1526 */ 1527 upload: function(id, params){ 1528 var len = this._queue.push(id); 1529 1530 var copy = {}; 1531 qq.extend(copy, params); 1532 this._params[id] = copy; 1533 1534 // if too many active uploads, wait... 1535 if (len <= this._options.maxConnections){ 1536 this._upload(id, this._params[id]); 1537 } 1538 }, 1539 retry: function(id) { 1540 var i = qq.indexOf(this._queue, id); 1541 if (i >= 0) { 1542 this._upload(id, this._params[id]); 1543 } 1544 else { 1545 this.upload(id, this._params[id]); 1546 } 1547 }, 1548 /** 1549 * Cancels file upload by id 1550 */ 1551 cancel: function(id){ 1552 this.log('Cancelling ' + id); 1553 this._cancel(id); 1554 this._dequeue(id); 1555 }, 1556 /** 1557 * Cancells all uploads 1558 */ 1559 cancelAll: function(){ 1560 for (var i=0; i<this._queue.length; i++){ 1561 this._cancel(this._queue[i]); 1562 } 1563 this._queue = []; 1564 }, 1565 /** 1566 * Returns name of the file identified by id 1567 */ 1568 getName: function(id){}, 1569 /** 1570 * Returns size of the file identified by id 1571 */ 1572 getSize: function(id){}, 1573 /** 1574 * Returns id of files being uploaded or 1575 * waiting for their turn 1576 */ 1577 getQueue: function(){ 1578 return this._queue; 1579 }, 1580 reset: function() { 1581 this.log('Resetting upload handler'); 1582 this._queue = []; 1583 this._params = []; 1584 }, 1585 /** 1586 * Actual upload method 1587 */ 1588 _upload: function(id){}, 1589 /** 1590 * Actual cancel method 1591 */ 1592 _cancel: function(id){}, 1593 /** 1594 * Removes element from queue, starts upload of next 1595 */ 1596 _dequeue: function(id){ 1597 var i = qq.indexOf(this._queue, id); 1598 this._queue.splice(i, 1); 1599 1600 var max = this._options.maxConnections; 1601 1602 if (this._queue.length >= max && i < max){ 1603 var nextId = this._queue[max-1]; 1604 this._upload(nextId, this._params[nextId]); 1605 } 1606 }, 1607 /** 1608 * Determine if the file exists. 1609 */ 1610 isValid: function(id) {} 1611}; 1612/** 1613 * Class for uploading files using form and iframe 1614 * @inherits qq.UploadHandlerAbstract 1615 */ 1616qq.UploadHandlerForm = function(o){ 1617 qq.UploadHandlerAbstract.apply(this, arguments); 1618 1619 this._inputs = {}; 1620 this._detach_load_events = {}; 1621}; 1622// @inherits qq.UploadHandlerAbstract 1623qq.extend(qq.UploadHandlerForm.prototype, qq.UploadHandlerAbstract.prototype); 1624 1625qq.extend(qq.UploadHandlerForm.prototype, { 1626 add: function(fileInput){ 1627 fileInput.setAttribute('name', this._options.inputName); 1628 var id = 'qq-upload-handler-iframe' + qq.getUniqueId(); 1629 1630 this._inputs[id] = fileInput; 1631 1632 // remove file input from DOM 1633 if (fileInput.parentNode){ 1634 qq(fileInput).remove(); 1635 } 1636 1637 return id; 1638 }, 1639 getName: function(id){ 1640 // get input value and remove path to normalize 1641 return this._inputs[id].value.replace(/.*(\/|\\)/, ""); 1642 }, 1643 isValid: function(id) { 1644 return this._inputs[id] !== undefined; 1645 }, 1646 reset: function() { 1647 qq.UploadHandlerAbstract.prototype.reset.apply(this, arguments); 1648 this._inputs = {}; 1649 this._detach_load_events = {}; 1650 }, 1651 _cancel: function(id){ 1652 this._options.onCancel(id, this.getName(id)); 1653 1654 delete this._inputs[id]; 1655 delete this._detach_load_events[id]; 1656 1657 var iframe = document.getElementById(id); 1658 if (iframe){ 1659 // to cancel request set src to something else 1660 // we use src="javascript:false;" because it doesn't 1661 // trigger ie6 prompt on https 1662 iframe.setAttribute('src', 'javascript:false;'); 1663 1664 qq(iframe).remove(); 1665 } 1666 }, 1667 _upload: function(id, params){ 1668 this._options.onUpload(id, this.getName(id), false); 1669 var input = this._inputs[id]; 1670 1671 if (!input){
1672 throw new Error('file with passed id was not added, or already uploaded or cancelled'); 1673 } 1674 1675 var fileName = this.getName(id); 1676 params[this._options.inputName] = fileName; 1677 1678 var iframe = this._createIframe(id); 1679 var form = this._createForm(iframe, params); 1680 form.appendChild(input); 1681 1682 var self = this; 1683 this._attachLoadEvent(iframe, function(){ 1684 self.log('iframe loaded'); 1685 1686 var response = self._getIframeContentJSON(iframe); 1687 1688 // timeout added to fix busy state in FF3.6 1689 setTimeout(function(){ 1690 self._detach_load_events[id](); 1691 delete self._detach_load_events[id]; 1692 qq(iframe).remove(); 1693 }, 1); 1694 1695 if (!response.success) { 1696 if (self._options.onAutoRetry(id, fileName, response)) { 1697 return; 1698 } 1699 } 1700 self._options.onComplete(id, fileName, response); 1701 self._dequeue(id); 1702 }); 1703 1704 this.log('Sending upload request for ' + id); 1705 form.submit(); 1706 qq(form).remove(); 1707 1708 return id; 1709 }, 1710 _attachLoadEvent: function(iframe, callback){ 1711 var self = this; 1712 this._detach_load_events[iframe.id] = qq(iframe).attach('load', function(){ 1713 self.log('Received response for ' + iframe.id); 1714 1715 // when we remove iframe from dom 1716 // the request stops, but in IE load 1717 // event fires 1718 if (!iframe.parentNode){ 1719 return; 1720 } 1721 1722 try { 1723 // fixing Opera 10.53 1724 if (iframe.contentDocument && 1725 iframe.contentDocument.body && 1726 iframe.contentDocument.body.innerHTML == "false"){ 1727 // In Opera event is fired second time 1728 // when body.innerHTML changed from false 1729 // to server response approx. after 1 sec 1730 // when we upload file with iframe 1731 return; 1732 } 1733 } 1734 catch (error) { 1735 //IE may throw an "access is denied" error when attempting to access contentDocument on the iframe in some cases 1736 self.log('Error when attempting to access iframe during handling of upload response (' + error + ")", 'error'); 1737 } 1738 1739 callback(); 1740 }); 1741 }, 1742 /** 1743 * Returns json object received by iframe from server. 1744 */ 1745 _getIframeContentJSON: function(iframe){ 1746 //IE may throw an "access is denied" error when attempting to access contentDocument on the iframe in some cases 1747 try { 1748 // iframe.contentWindow.document - for IE<7 1749 var doc = iframe.contentDocument ? iframe.contentDocument: iframe.contentWindow.document, 1750 response; 1751 1752 var innerHTML = doc.body.innerHTML; 1753 this.log("converting iframe's innerHTML to JSON"); 1754 this.log("innerHTML = " + innerHTML); 1755 //plain text response may be wrapped in <pre> tag 1756 if (innerHTML && innerHTML.match(/^<pre/i)) { 1757 innerHTML = doc.body.firstChild.firstChild.nodeValue; 1758 } 1759 response = eval("(" + innerHTML + ")"); 1760 } catch(error){ 1761 this.log('Error when attempting to parse form upload response (' + error + ")", 'error'); 1762 response = {success: false}; 1763 } 1764 1765 return response; 1766 }, 1767 /** 1768 * Creates iframe with unique name 1769 */ 1770 _createIframe: function(id){ 1771 // We can't use following code as the name attribute 1772 // won't be properly registered in IE6, and new window 1773 // on form submit will open 1774 // var iframe = document.createElement('iframe'); 1775 // iframe.setAttribute('name', id); 1776 1777 var iframe = qq.toElement('<iframe src="javascript:false;" name="' + id + '" />'); 1778 // src="javascript:false;" removes ie6 prompt on https 1779 1780 iframe.setAttribute('id', id); 1781 1782 iframe.style.display = 'none'; 1783 document.body.appendChild(iframe); 1784 1785 return iframe; 1786 }, 1787 /** 1788 * Creates form, that will be submitted to iframe 1789 */ 1790 _createForm: function(iframe, params){ 1791 // We can't use the following code in IE6 1792 // var form = document.createElement('form'); 1793 // form.setAttribute('method', 'post'); 1794 // form.setAttribute('enctype', 'multipart/form-data'); 1795 // Because in this case file won't be attached to request 1796 var protocol = this._options.demoMode ? "GET" : "POST" 1797 var form = qq.toElement('<form method="' + protocol + '" enctype="multipart/form-data"></form>'); 1798 1799 var queryString = qq.obj2url(params, this._options.endpoint); 1800 1801 form.setAttribute('action', queryString); 1802 form.setAttribute('target', iframe.name); 1803 form.style.display = 'none'; 1804 document.body.appendChild(form); 1805 1806 return form; 1807 } 1808}); 1809/** 1810 * Class for uploading files using xhr 1811 * @inherits qq.UploadHandlerAbstract 1812 */ 1813qq.UploadHandlerXhr = function(o){ 1814 qq.UploadHandlerAbstract.apply(this, arguments); 1815 1816 this._files = []; 1817 this._xhrs = []; 1818 1819 // current loaded size in bytes for each file 1820 this._loaded = []; 1821}; 1822 1823// static method 1824qq.UploadHandlerXhr.isSupported = function(){ 1825 var input = document.createElement('input'); 1826 input.type = 'file'; 1827 1828 return ( 1829 'multiple' in input && 1830 typeof File != "undefined" && 1831 typeof FormData != "undefined" && 1832 typeof (new XMLHttpRequest()).upload != "undefined" ); 1833}; 1834 1835// @inherits qq.UploadHandlerAbstract 1836qq.extend(qq.UploadHandlerXhr.prototype, qq.UploadHandlerAbstract.prototype) 1837 1838qq.extend(qq.UploadHandlerXhr.prototype, { 1839 /** 1840 * Adds file to the queue 1841 * Returns id to use with upload, cancel 1842 **/ 1843 add: function(file){ 1844 if (!(file instanceof File)){ 1845 throw new Error('Passed obj in not a File (in qq.UploadHandlerXhr)'); 1846 } 1847 1848 return this._files.push(file) - 1; 1849 }, 1850 getName: function(id){ 1851 var file = this._files[id]; 1852 // fix missing name in Safari 4 1853 //NOTE: fixed missing name firefox 11.0a2 file.fileName is actually undefined 1854 return (file.fileName !== null && file.fileName !== undefined) ? file.fileName : file.name; 1855 }, 1856 getSize: function(id){ 1857 var file = this._files[id]; 1858 return file.fileSize != null ? file.fileSize : file.size; 1859 }, 1860 /** 1861 * Returns uploaded bytes for file identified by id 1862 */ 1863 getLoaded: function(id){ 1864 return this._loaded[id] || 0; 1865 }, 1866 isValid: function(id) { 1867 return this._files[id] !== undefined; 1868 }, 1869 reset: function() { 1870 qq.UploadHandlerAbstract.prototype.reset.apply(this, arguments); 1871 this._files = []; 1872 this._xhrs = []; 1873 this._loaded = []; 1874 }, 1875 /** 1876 * Sends the file identified by id and additional query params to the server 1877 * @param {Object} params name-value string pairs 1878 */ 1879 _upload: function(id, params){ 1880 this._options.onUpload(id, this.getName(id), true); 1881 1882 var file = this._files[id], 1883 name = this.getName(id),
1884 size = this.getSize(id); 1885 1886 this._loaded[id] = 0; 1887 1888 var xhr = this._xhrs[id] = new XMLHttpRequest(); 1889 var self = this; 1890 1891 xhr.upload.onprogress = function(e){ 1892 if (e.lengthComputable){ 1893 self._loaded[id] = e.loaded; 1894 self._options.onProgress(id, name, e.loaded, e.total); 1895 } 1896 }; 1897 1898 xhr.onreadystatechange = function(){ 1899 if (xhr.readyState == 4){ 1900 self._onComplete(id, xhr); 1901 } 1902 }; 1903 1904 // build query string 1905 params = params || {}; 1906 params[this._options.inputName] = name; 1907 var queryString = qq.obj2url(params, this._options.endpoint); 1908 1909 var protocol = this._options.demoMode ? "GET" : "POST"; 1910 xhr.open(protocol, queryString, true); 1911 xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); 1912 xhr.setRequestHeader("X-File-Name", encodeURIComponent(name)); 1913 xhr.setRequestHeader("Cache-Control", "no-cache"); 1914 if (this._options.forceMultipart) { 1915 var formData = new FormData(); 1916 formData.append(this._options.inputName, file); 1917 file = formData; 1918 } else { 1919 xhr.setRequestHeader("Content-Type", "application/octet-stream"); 1920 //NOTE: return mime type in xhr works on chrome 16.0.9 firefox 11.0a2 1921 xhr.setRequestHeader("X-Mime-Type",file.type ); 1922 } 1923 for (key in this._options.customHeaders){ 1924 xhr.setRequestHeader(key, this._options.customHeaders[key]); 1925 }; 1926 1927 this.log('Sending upload request for ' + id); 1928 xhr.send(file); 1929 }, 1930 _onComplete: function(id, xhr){ 1931 "use strict"; 1932 // the request was aborted/cancelled 1933 if (!this._files[id]) { return; } 1934 1935 var name = this.getName(id); 1936 var size = this.getSize(id); 1937 var response; //the parsed JSON response from the server, or the empty object if parsing failed. 1938 1939 this._options.onProgress(id, name, size, size); 1940 1941 this.log("xhr - server response received for " + id); 1942 this.log("responseText = " + xhr.responseText); 1943 1944 try { 1945 if (typeof JSON.parse === "function") { 1946 response = JSON.parse(xhr.responseText); 1947 } else { 1948 response = eval("(" + xhr.responseText + ")"); 1949 } 1950 } catch(error){ 1951 this.log('Error when attempting to parse xhr response text (' + error + ')', 'error'); 1952 response = {}; 1953 } 1954 1955 if (xhr.status !== 200 || !response.success){ 1956 if (this._options.onAutoRetry(id, name, response, xhr)) { 1957 return; 1958 } 1959 } 1960 1961 this._options.onComplete(id, name, response, xhr); 1962 1963 this._xhrs[id] = null; 1964 this._dequeue(id); 1965 }, 1966 _cancel: function(id){ 1967 this._options.onCancel(id, this.getName(id)); 1968 1969 this._files[id] = null; 1970 1971 if (this._xhrs[id]){ 1972 this._xhrs[id].abort(); 1973 this._xhrs[id] = null; 1974 } 1975 } 1976}); 1977(function($) { 1978 "use strict"; 1979 var uploader, $el, init, dataStore, pluginOption, pluginOptions, addCallbacks, transformOptions, isValidCommand, 1980 delegateCommand; 1981 1982 pluginOptions = ['uploaderType']; 1983 1984 init = function (options) { 1985 if (options) { 1986 var xformedOpts = transformOptions(options); 1987 addCallbacks(xformedOpts); 1988 1989 if (pluginOption('uploaderType') === 'basic') { 1990 uploader(new qq.FineUploaderBasic(xformedOpts)); 1991 } 1992 else { 1993 uploader(new qq.FineUploader(xformedOpts)); 1994 } 1995 } 1996 1997 return $el; 1998 }; 1999 2000 dataStore = function(key, val) { 2001 var data = $el.data('fineuploader'); 2002 2003 if (val) { 2004 if (data === undefined) { 2005 data = {}; 2006 } 2007 data[key] = val; 2008 $el.data('fineuploader', data); 2009 } 2010 else { 2011 if (data === undefined) { 2012 return null; 2013 } 2014 return data[key]; 2015 } 2016 }; 2017 2018 //the underlying Fine Uploader instance is stored in jQuery's data stored, associated with the element 2019 // tied to this instance of the plug-in 2020 uploader = function(instanceToStore) { 2021 return dataStore('uploader', instanceToStore); 2022 }; 2023 2024 pluginOption = function(option, optionVal) { 2025 return dataStore(option, optionVal); 2026 }; 2027 2028 //implement all callbacks defined in Fine Uploader as functions that trigger appropriately names events and 2029 // return the result of executing the bound handler back to Fine Uploader 2030 addCallbacks = function(transformedOpts) { 2031 var callbacks = transformedOpts.callbacks = {}; 2032 2033 $.each(new qq.FineUploaderBasic()._options.callbacks, function(prop, func) { 2034 var name, $callbackEl; 2035 2036 name = /^on(\w+)/.exec(prop)[1]; 2037 name = name.substring(0, 1).toLowerCase() + name.substring(1); 2038 $callbackEl = $el; 2039 2040 callbacks[prop] = function() { 2041 var args = Array.prototype.slice.call(arguments); 2042 return $callbackEl.triggerHandler(name, args); 2043 }; 2044 }); 2045 }; 2046 2047 //transform jQuery objects into HTMLElements, and pass along all other option properties 2048 transformOptions = function(source, dest) { 2049 var xformed, arrayVals; 2050 2051 if (dest === undefined) { 2052 if (source.uploaderType !== 'basic') { 2053 xformed = { element : $el[0] }; 2054 } 2055 else { 2056 xformed = {}; 2057 } 2058 } 2059 else { 2060 xformed = dest; 2061 } 2062 2063 $.each(source, function(prop, val) { 2064 if ($.inArray(prop, pluginOptions) >= 0) { 2065 pluginOption(prop, val); 2066 } 2067 else if (val instanceof $) { 2068 xformed[prop] = val[0]; 2069 } 2070 else if ($.isPlainObject(val)) { 2071 xformed[prop] = {}; 2072 transformOptions(val, xformed[prop]); 2073 } 2074 else if ($.isArray(val)) { 2075 arrayVals = []; 2076 $.each(val, function(idx, arrayVal) { 2077 if (arrayVal instanceof $) { 2078 $.merge(arrayVals, arrayVal); 2079 } 2080 else { 2081 arrayVals.push(arrayVal); 2082 } 2083 }); 2084 xformed[prop] = arrayVals; 2085 } 2086 else { 2087 xformed[prop] = val; 2088 } 2089 }); 2090 2091 if (dest === undefined) { 2092 return xformed; 2093 } 2094 }; 2095 2096 isValidCommand = function(command) { 2097 return $.type(command) === "string" && 2098 !command.match(/^_/) && //enforce private methods convention 2099 uploader()[command] !== undefined; 2100 }; 2101 2102 //assuming we have already verified that this is a valid command, call the associated function in the underlying 2103 // Fine Uploader instance (passing along the arguments from the caller) and return the result of the call back to the caller 2104 delegateCommand = function(command) { 2105 return uploader()[command].apply(uploader(), Array.prototype.slice.call(arguments, 1)); 2106 }; 2107 2108 $.fn.fineUploader = function(optionsOrCommand) { 2109 $el = this; 2110 2111 if (uploader() && isValidCommand(optionsOrCommand)) { 2112 return delegateCommand.apply(this, arguments); 2113 } 2114 else if (typeof optionsOrCommand === 'object' || !optionsOrCommand) {
2115 return init.apply(this, arguments); 2116 } 2117 else { 2118 $.error('Method ' + optionsOrCommand + ' does not exist on jQuery.fineUploader'); 2119 } 2120 2121 return this; 2122 }; 2123 2124}(jQuery));
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.