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