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