/js/lib/ajaxupload.js
JavaScript | 679 lines | 371 code | 93 blank | 215 comment | 86 complexity | ad9135b2a5aa39fefffe257e5fb8f11c MD5 | raw file
1/** 2 * AJAX Upload ( http://valums.com/ajax-upload/ ) 3 * Copyright (c) Andris Valums 4 * Licensed under the MIT license ( http://valums.com/mit-license/ ) 5 * Thanks to Gary Haran, David Mark, Corey Burns and others for contributions 6 */ 7(function () { 8 /* global window */ 9 /* jslint browser: true, devel: true, undef: true, nomen: true, bitwise: true, regexp: true, newcap: true, immed: true */ 10 11 /** 12 * Wrapper for FireBug's console.log 13 */ 14 function log(){ 15 if (typeof(console) != 'undefined' && typeof(console.log) == 'function'){ 16 Array.prototype.unshift.call(arguments, '[Ajax Upload]'); 17 console.log( Array.prototype.join.call(arguments, ' ')); 18 } 19 } 20 21 /** 22 * Attaches event to a dom element. 23 * @param {Element} el 24 * @param type event name 25 * @param fn callback This refers to the passed element 26 */ 27 function addEvent(el, type, fn){ 28 if (el.addEventListener) { 29 el.addEventListener(type, fn, false); 30 } else if (el.attachEvent) { 31 el.attachEvent('on' + type, function(){ 32 fn.call(el); 33 }); 34 } else { 35 throw new Error('not supported or DOM not loaded'); 36 } 37 } 38 39 /** 40 * Attaches resize event to a window, limiting 41 * number of event fired. Fires only when encounteres 42 * delay of 100 after series of events. 43 * 44 * Some browsers fire event multiple times when resizing 45 * http://www.quirksmode.org/dom/events/resize.html 46 * 47 * @param fn callback This refers to the passed element 48 */ 49 function addResizeEvent(fn){ 50 var timeout; 51 52 addEvent(window, 'resize', function(){ 53 if (timeout){ 54 clearTimeout(timeout); 55 } 56 timeout = setTimeout(fn, 100); 57 }); 58 } 59 60 // Needs more testing, will be rewriten for next version 61 // getOffset function copied from jQuery lib (http://jquery.com/) 62 if (document.documentElement.getBoundingClientRect){ 63 // Get Offset using getBoundingClientRect 64 // http://ejohn.org/blog/getboundingclientrect-is-awesome/ 65 var getOffset = function(el){ 66 var box = el.getBoundingClientRect(); 67 var doc = el.ownerDocument; 68 var body = doc.body; 69 var docElem = doc.documentElement; // for ie 70 var clientTop = docElem.clientTop || body.clientTop || 0; 71 var clientLeft = docElem.clientLeft || body.clientLeft || 0; 72 73 // In Internet Explorer 7 getBoundingClientRect property is treated as physical, 74 // while others are logical. Make all logical, like in IE8. 75 var zoom = 1; 76 if (body.getBoundingClientRect) { 77 var bound = body.getBoundingClientRect(); 78 zoom = (bound.right - bound.left) / body.clientWidth; 79 } 80 81 if (zoom > 1) { 82 clientTop = 0; 83 clientLeft = 0; 84 } 85 86 var top = box.top / zoom + (window.pageYOffset || docElem && docElem.scrollTop / zoom || body.scrollTop / zoom) - clientTop, left = box.left / zoom + (window.pageXOffset || docElem && docElem.scrollLeft / zoom || body.scrollLeft / zoom) - clientLeft; 87 88 return { 89 top: top, 90 left: left 91 }; 92 }; 93 } else { 94 // Get offset adding all offsets 95 var getOffset = function(el){ 96 var top = 0, left = 0; 97 do { 98 top += el.offsetTop || 0; 99 left += el.offsetLeft || 0; 100 el = el.offsetParent; 101 } while (el); 102 103 return { 104 left: left, 105 top: top 106 }; 107 }; 108 } 109 110 /** 111 * Returns left, top, right and bottom properties describing the border-box, 112 * in pixels, with the top-left relative to the body 113 * @param {Element} el 114 * @return {Object} Contains left, top, right,bottom 115 */ 116 function getBox(el){ 117 var left, right, top, bottom; 118 var offset = getOffset(el); 119 left = offset.left; 120 top = offset.top; 121 122 right = left + el.offsetWidth; 123 bottom = top + el.offsetHeight; 124 125 return { 126 left: left, 127 right: right, 128 top: top, 129 bottom: bottom 130 }; 131 } 132 133 /** 134 * Helper that takes object literal 135 * and add all properties to element.style 136 * @param {Element} el 137 * @param {Object} styles 138 */ 139 function addStyles(el, styles){ 140 for (var name in styles) { 141 if (styles.hasOwnProperty(name)) { 142 el.style[name] = styles[name]; 143 } 144 } 145 } 146 147 /** 148 * Function places an absolutely positioned 149 * element on top of the specified element 150 * copying position and dimentions. 151 * @param {Element} from 152 * @param {Element} to 153 */ 154 function copyLayout(from, to){ 155 var box = getBox(from); 156 157 addStyles(to, { 158 position: 'absolute', 159 left : box.left + 'px', 160 top : box.top + 'px', 161 width : from.offsetWidth + 'px', 162 height : from.offsetHeight + 'px' 163 }); 164 } 165 166 /** 167 * Creates and returns element from html chunk 168 * Uses innerHTML to create an element 169 */ 170 var toElement = (function(){ 171 var div = document.createElement('div'); 172 return function(html){ 173 div.innerHTML = html; 174 var el = div.firstChild; 175 return div.removeChild(el); 176 }; 177 })(); 178 179 /** 180 * Function generates unique id 181 * @return unique id 182 */ 183 var getUID = (function(){ 184 var id = 0; 185 return function(){ 186 return 'ValumsAjaxUpload' + id++; 187 }; 188 })(); 189 190 /** 191 * Get file name from path 192 * @param {String} file path to file 193 * @return filename 194 */ 195 function fileFromPath(file){ 196 return file.replace(/.*(\/|\\)/, ""); 197 } 198 199 /** 200 * Get file extension lowercase 201 * @param {String} file name 202 * @return file extenstion 203 */ 204 function getExt(file){ 205 return (-1 !== file.indexOf('.')) ? file.replace(/.*[.]/, '') : ''; 206 } 207 208 function hasClass(el, name){ 209 var re = new RegExp('\\b' + name + '\\b'); 210 return re.test(el.className); 211 } 212 function addClass(el, name){ 213 if ( ! hasClass(el, name)){ 214 el.className += ' ' + name; 215 } 216 } 217 function removeClass(el, name){ 218 var re = new RegExp('\\b' + name + '\\b'); 219 el.className = el.className.replace(re, ''); 220 } 221 222 function removeNode(el){ 223 el.parentNode.removeChild(el); 224 } 225 226 /** 227 * Easy styling and uploading 228 * @constructor 229 * @param button An element you want convert to 230 * upload button. Tested dimentions up to 500x500px 231 * @param {Object} options See defaults below. 232 */ 233 window.AjaxUpload = function(button, options){ 234 this._settings = { 235 // Location of the server-side upload script 236 action: 'upload.php', 237 // File upload name 238 name: 'userfile', 239 // Additional data to send 240 data: {}, 241 // Submit file as soon as it's selected 242 autoSubmit: true, 243 // The type of data that you're expecting back from the server. 244 // html and xml are detected automatically. 245 // Only useful when you are using json data as a response. 246 // Set to "json" in that case. 247 responseType: false, 248 // Class applied to button when mouse is hovered 249 hoverClass: 'hover', 250 // Class applied to button when AU is disabled 251 disabledClass: 'disabled', 252 // When user selects a file, useful with autoSubmit disabled 253 // You can return false to cancel upload 254 onChange: function(file, extension){ 255 }, 256 // Callback to fire before file is uploaded 257 // You can return false to cancel upload 258 onSubmit: function(file, extension){ 259 }, 260 // Fired when file upload is completed 261 // WARNING! DO NOT USE "FALSE" STRING AS A RESPONSE! 262 onComplete: function(file, response){ 263 } 264 }; 265 266 // Merge the users options with our defaults 267 for (var i in options) { 268 if (options.hasOwnProperty(i)){ 269 this._settings[i] = options[i]; 270 } 271 } 272 273 // button isn't necessary a dom element 274 if (button.jquery){ 275 // jQuery object was passed 276 button = button[0]; 277 } else if (typeof button == "string") { 278 if (/^#.*/.test(button)){ 279 // If jQuery user passes #elementId don't break it 280 button = button.slice(1); 281 } 282 283 button = document.getElementById(button); 284 } 285 286 if ( ! button || button.nodeType !== 1){ 287 throw new Error("Please make sure that you're passing a valid element"); 288 } 289 290 if ( button.nodeName.toUpperCase() == 'A'){ 291 // disable link 292 addEvent(button, 'click', function(e){ 293 if (e && e.preventDefault){ 294 e.preventDefault(); 295 } else if (window.event){ 296 window.event.returnValue = false; 297 } 298 }); 299 } 300 301 // DOM element 302 this._button = button; 303 // DOM element 304 this._input = null; 305 // If disabled clicking on button won't do anything 306 this._disabled = false; 307 308 // if the button was disabled before refresh if will remain 309 // disabled in FireFox, let's fix it 310 this.enable(); 311 312 this._rerouteClicks(); 313 }; 314 315 // assigning methods to our class 316 AjaxUpload.prototype = { 317 setData: function(data){ 318 this._settings.data = data; 319 }, 320 disable: function(){ 321 addClass(this._button, this._settings.disabledClass); 322 this._disabled = true; 323 324 var nodeName = this._button.nodeName.toUpperCase(); 325 if (nodeName == 'INPUT' || nodeName == 'BUTTON'){ 326 this._button.setAttribute('disabled', 'disabled'); 327 } 328 329 // hide input 330 if (this._input){ 331 // We use visibility instead of display to fix problem with Safari 4 332 // The problem is that the value of input doesn't change if it 333 // has display none when user selects a file 334 this._input.parentNode.style.visibility = 'hidden'; 335 } 336 }, 337 enable: function(){ 338 removeClass(this._button, this._settings.disabledClass); 339 this._button.removeAttribute('disabled'); 340 this._disabled = false; 341 342 }, 343 /** 344 * Creates invisible file input 345 * that will hover above the button 346 * <div><input type='file' /></div> 347 */ 348 _createInput: function(){ 349 var self = this; 350 351 var input = document.createElement("input"); 352 input.setAttribute('type', 'file'); 353 input.setAttribute('name', this._settings.name); 354 355 addStyles(input, { 356 'position' : 'absolute', 357 // in Opera only 'browse' button 358 // is clickable and it is located at 359 // the right side of the input 360 'right' : 0, 361 'margin' : 0, 362 'padding' : 0, 363 'fontSize' : '480px', 364 'cursor' : 'pointer' 365 }); 366 367 var div = document.createElement("div"); 368 addStyles(div, { 369 'display' : 'block', 370 'position' : 'absolute', 371 'overflow' : 'hidden', 372 'margin' : 0, 373 'padding' : 0, 374 'opacity' : 0, 375 // Make sure browse button is in the right side 376 // in Internet Explorer 377 'direction' : 'ltr', 378 //Max zIndex supported by Opera 9.0-9.2 379 'zIndex': 2147483583 380 }); 381 382 // Make sure that element opacity exists. 383 // Otherwise use IE filter 384 if ( div.style.opacity !== "0") { 385 if (typeof(div.filters) == 'undefined'){ 386 throw new Error('Opacity not supported by the browser'); 387 } 388 div.style.filter = "alpha(opacity=0)"; 389 } 390 391 addEvent(input, 'change', function(){ 392 393 if ( ! input || input.value === ''){ 394 return; 395 } 396 397 // Get filename from input, required 398 // as some browsers have path instead of it 399 var file = fileFromPath(input.value); 400 401 if (false === self._settings.onChange.call(self, file, getExt(file))){ 402 self._clearInput(); 403 return; 404 } 405 406 // Submit form when value is changed 407 if (self._settings.autoSubmit) { 408 self.submit(); 409 } 410 }); 411 412 addEvent(input, 'mouseover', function(){ 413 addClass(self._button, self._settings.hoverClass); 414 }); 415 416 addEvent(input, 'mouseout', function(){ 417 removeClass(self._button, self._settings.hoverClass); 418 419 // We use visibility instead of display to fix problem with Safari 4 420 // The problem is that the value of input doesn't change if it 421 // has display none when user selects a file 422 input.parentNode.style.visibility = 'hidden'; 423 424 }); 425 426 div.appendChild(input); 427 document.body.appendChild(div); 428 429 this._input = input; 430 }, 431 _clearInput : function(){ 432 if (!this._input){ 433 return; 434 } 435 436 // this._input.value = ''; Doesn't work in IE6 437 removeNode(this._input.parentNode); 438 this._input = null; 439 this._createInput(); 440 441 removeClass(this._button, this._settings.hoverClass); 442 }, 443 /** 444 * Function makes sure that when user clicks upload button, 445 * the this._input is clicked instead 446 */ 447 _rerouteClicks: function(){ 448 var self = this; 449 450 // IE will later display 'access denied' error 451 // if you use using self._input.click() 452 // other browsers just ignore click() 453 454 addEvent(self._button, 'mouseover', function(){ 455 if (self._disabled){ 456 return; 457 } 458 459 if ( ! self._input){ 460 self._createInput(); 461 } 462 463 var div = self._input.parentNode; 464 copyLayout(self._button, div); 465 div.style.visibility = 'visible'; 466 467 }); 468 469 470 // commented because we now hide input on mouseleave 471 /** 472 * When the window is resized the elements 473 * can be misaligned if button position depends 474 * on window size 475 */ 476 //addResizeEvent(function(){ 477 // if (self._input){ 478 // copyLayout(self._button, self._input.parentNode); 479 // } 480 //}); 481 482 }, 483 /** 484 * Creates iframe with unique name 485 * @return {Element} iframe 486 */ 487 _createIframe: function(){ 488 // We can't use getTime, because it sometimes return 489 // same value in safari :( 490 var id = getUID(); 491 492 // We can't use following code as the name attribute 493 // won't be properly registered in IE6, and new window 494 // on form submit will open 495 // var iframe = document.createElement('iframe'); 496 // iframe.setAttribute('name', id); 497 498 var iframe = toElement('<iframe src="javascript:false;" name="' + id + '" />'); 499 // src="javascript:false; was added 500 // because it possibly removes ie6 prompt 501 // "This page contains both secure and nonsecure items" 502 // Anyway, it doesn't do any harm. 503 iframe.setAttribute('id', id); 504 505 iframe.style.display = 'none'; 506 document.body.appendChild(iframe); 507 508 return iframe; 509 }, 510 /** 511 * Creates form, that will be submitted to iframe 512 * @param {Element} iframe Where to submit 513 * @return {Element} form 514 */ 515 _createForm: function(iframe){ 516 var settings = this._settings; 517 518 // We can't use the following code in IE6 519 // var form = document.createElement('form'); 520 // form.setAttribute('method', 'post'); 521 // form.setAttribute('enctype', 'multipart/form-data'); 522 // Because in this case file won't be attached to request 523 var form = toElement('<form method="post" enctype="multipart/form-data"></form>'); 524 525 form.setAttribute('action', settings.action); 526 form.setAttribute('target', iframe.name); 527 form.style.display = 'none'; 528 document.body.appendChild(form); 529 530 // Create hidden input element for each data key 531 for (var prop in settings.data) { 532 if (settings.data.hasOwnProperty(prop)){ 533 var el = document.createElement("input"); 534 el.setAttribute('type', 'hidden'); 535 el.setAttribute('name', prop); 536 el.setAttribute('value', settings.data[prop]); 537 form.appendChild(el); 538 } 539 } 540 return form; 541 }, 542 /** 543 * Gets response from iframe and fires onComplete event when ready 544 * @param iframe 545 * @param file Filename to use in onComplete callback 546 */ 547 _getResponse : function(iframe, file){ 548 // getting response 549 var toDeleteFlag = false, self = this, settings = this._settings; 550 551 addEvent(iframe, 'load', function(){ 552 553 if (// For Safari 554 iframe.src == "javascript:'%3Chtml%3E%3C/html%3E';" || 555 // For FF, IE 556 iframe.src == "javascript:'<html></html>';"){ 557 // First time around, do not delete. 558 // We reload to blank page, so that reloading main page 559 // does not re-submit the post. 560 561 if (toDeleteFlag) { 562 // Fix busy state in FF3 563 setTimeout(function(){ 564 removeNode(iframe); 565 }, 0); 566 } 567 568 return; 569 } 570 571 var doc = iframe.contentDocument ? iframe.contentDocument : window.frames[iframe.id].document; 572 573 // fixing Opera 9.26,10.00 574 if (doc.readyState && doc.readyState != 'complete') { 575 // Opera fires load event multiple times 576 // Even when the DOM is not ready yet 577 // this fix should not affect other browsers 578 return; 579 } 580 581 // fixing Opera 9.64 582 if (doc.body && doc.body.innerHTML == "false") { 583 // In Opera 9.64 event was fired second time 584 // when body.innerHTML changed from false 585 // to server response approx. after 1 sec 586 return; 587 } 588 589 var response; 590 591 if (doc.XMLDocument) { 592 // response is a xml document Internet Explorer property 593 response = doc.XMLDocument; 594 } else if (doc.body){ 595 // response is html document or plain text 596 response = doc.body.innerHTML; 597 598 if (settings.responseType && settings.responseType.toLowerCase() == 'json') { 599 // If the document was sent as 'application/javascript' or 600 // 'text/javascript', then the browser wraps the text in a <pre> 601 // tag and performs html encoding on the contents. In this case, 602 // we need to pull the original text content from the text node's 603 // nodeValue property to retrieve the unmangled content. 604 // Note that IE6 only understands text/html 605 if (doc.body.firstChild && doc.body.firstChild.nodeName.toUpperCase() == 'PRE') { 606 response = doc.body.firstChild.firstChild.nodeValue; 607 } 608 609 if (response) { 610 response = eval("(" + response + ")"); 611 } else { 612 response = {}; 613 } 614 } 615 } else { 616 // response is a xml document 617 response = doc; 618 } 619 620 settings.onComplete.call(self, file, response); 621 622 // Reload blank page, so that reloading main page 623 // does not re-submit the post. Also, remember to 624 // delete the frame 625 toDeleteFlag = true; 626 627 // Fix IE mixed content issue 628 iframe.src = "javascript:'<html></html>';"; 629 }); 630 }, 631 /** 632 * Upload file contained in this._input 633 */ 634 submit: function(){ 635 var self = this, settings = this._settings; 636 637 if ( ! this._input || this._input.value === ''){ 638 return; 639 } 640 641 var file = fileFromPath(this._input.value); 642 643 // user returned false to cancel upload 644 if (false === settings.onSubmit.call(this, file, getExt(file))){ 645 this._clearInput(); 646 return; 647 } 648 649 // sending request 650 var iframe = this._createIframe(); 651 var form = this._createForm(iframe); 652 653 this._settings._iframe = iframe; 654 655 // assuming following structure 656 // div -> input type='file' 657 removeNode(this._input.parentNode); 658 removeClass(self._button, self._settings.hoverClass); 659 660 form.appendChild(this._input); 661 662 form.submit(); 663 664 // request set, clean up 665 removeNode(form); form = null; 666 removeNode(this._input); this._input = null; 667 668 // Get response from iframe and fire onComplete event when ready 669 this._getResponse(iframe, file); 670 671 // get ready for next request 672 this._createInput(); 673 }, 674 675 cancel: function () { 676 this._settings._iframe.src = "javascript:'<html></html>';"; 677 } 678 }; 679})();