diff options
Diffstat (limited to 'include')
-rw-r--r-- | include/ajaxupload.js | 691 |
1 files changed, 691 insertions, 0 deletions
diff --git a/include/ajaxupload.js b/include/ajaxupload.js new file mode 100644 index 000000000..f0fbfe6c2 --- /dev/null +++ b/include/ajaxupload.js @@ -0,0 +1,691 @@ +/** + * AJAX Upload ( http://valums.com/ajax-upload/ ) + * Copyright (c) Andris Valums + * Licensed under the MIT license ( http://valums.com/mit-license/ ) + * Thanks to Gary Haran, David Mark, Corey Burns and others for contributions. + */ +(function () { + /* global window */ + /* jslint browser: true, devel: true, undef: true, nomen: true, bitwise: true, regexp: true, newcap: true, immed: true */ + + /** + * Wrapper for FireBug's console.log + */ + function log(){ + if (typeof(console) != 'undefined' && typeof(console.log) == 'function'){ + Array.prototype.unshift.call(arguments, '[Ajax Upload]'); + console.log( Array.prototype.join.call(arguments, ' ')); + } + } + + /** + * Attaches event to a dom element. + * @param {Element} el + * @param type event name + * @param fn callback This refers to the passed element + */ + function addEvent(el, type, fn){ + if (el.addEventListener) { + el.addEventListener(type, fn, false); + } else if (el.attachEvent) { + el.attachEvent('on' + type, function(){ + fn.call(el); + }); + } else { + throw new Error('not supported or DOM not loaded'); + } + } + + /** + * Attaches resize event to a window, limiting + * number of event fired. Fires only when encounteres + * delay of 100 after series of events. + * + * Some browsers fire event multiple times when resizing + * http://www.quirksmode.org/dom/events/resize.html + * + * @param fn callback This refers to the passed element + */ + function addResizeEvent(fn){ + var timeout; + + addEvent(window, 'resize', function(){ + if (timeout){ + clearTimeout(timeout); + } + timeout = setTimeout(fn, 100); + }); + } + + // Needs more testing, will be rewriten for next version + // getOffset function copied from jQuery lib (http://jquery.com/) + if (document.documentElement.getBoundingClientRect){ + // Get Offset using getBoundingClientRect + // http://ejohn.org/blog/getboundingclientrect-is-awesome/ + var getOffset = function(el){ + var box = el.getBoundingClientRect(); + var doc = el.ownerDocument; + var body = doc.body; + var docElem = doc.documentElement; // for ie + var clientTop = docElem.clientTop || body.clientTop || 0; + var clientLeft = docElem.clientLeft || body.clientLeft || 0; + + // In Internet Explorer 7 getBoundingClientRect property is treated as physical, + // while others are logical. Make all logical, like in IE8. + var zoom = 1; + if (body.getBoundingClientRect) { + var bound = body.getBoundingClientRect(); + zoom = (bound.right - bound.left) / body.clientWidth; + } + + if (zoom > 1) { + clientTop = 0; + clientLeft = 0; + } + + 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; + + return { + top: top, + left: left + }; + }; + } else { + // Get offset adding all offsets + var getOffset = function(el){ + var top = 0, left = 0; + do { + top += el.offsetTop || 0; + left += el.offsetLeft || 0; + el = el.offsetParent; + } while (el); + + return { + left: left, + top: top + }; + }; + } + + /** + * Returns left, top, right and bottom properties describing the border-box, + * in pixels, with the top-left relative to the body + * @param {Element} el + * @return {Object} Contains left, top, right,bottom + */ + function getBox(el){ + var left, right, top, bottom; + var offset = getOffset(el); + left = offset.left; + top = offset.top; + + right = left + el.offsetWidth; + bottom = top + el.offsetHeight; + + return { + left: left, + right: right, + top: top, + bottom: bottom + }; + } + + /** + * Helper that takes object literal + * and add all properties to element.style + * @param {Element} el + * @param {Object} styles + */ + function addStyles(el, styles){ + for (var name in styles) { + if (styles.hasOwnProperty(name)) { + el.style[name] = styles[name]; + } + } + } + + /** + * Function places an absolutely positioned + * element on top of the specified element + * copying position and dimentions. + * @param {Element} from + * @param {Element} to + */ + function copyLayout(from, to){ + var box = getBox(from); + + addStyles(to, { + position: 'absolute', + left : box.left + 'px', + top : box.top + 'px', + width : from.offsetWidth + 'px', + height : from.offsetHeight + 'px' + }); + to.title = from.title; + } + + /** + * Creates and returns element from html chunk + * Uses innerHTML to create an element + */ + var toElement = (function(){ + var div = document.createElement('div'); + return function(html){ + div.innerHTML = html; + var el = div.firstChild; + return div.removeChild(el); + }; + })(); + + /** + * Function generates unique id + * @return unique id + */ + var getUID = (function(){ + var id = 0; + return function(){ + return 'ValumsAjaxUpload' + id++; + }; + })(); + + /** + * Get file name from path + * @param {String} file path to file + * @return filename + */ + function fileFromPath(file){ + return file.replace(/.*(\/|\\)/, ""); + } + + /** + * Get file extension lowercase + * @param {String} file name + * @return file extenstion + */ + function getExt(file){ + return (-1 !== file.indexOf('.')) ? file.replace(/.*[.]/, '') : ''; + } + + function hasClass(el, name){ + var re = new RegExp('\\b' + name + '\\b'); + return re.test(el.className); + } + function addClass(el, name){ + if ( ! hasClass(el, name)){ + el.className += ' ' + name; + } + } + function removeClass(el, name){ + var re = new RegExp('\\b' + name + '\\b'); + el.className = el.className.replace(re, ''); + } + + function removeNode(el){ + el.parentNode.removeChild(el); + } + + /** + * Easy styling and uploading + * @constructor + * @param button An element you want convert to + * upload button. Tested dimentions up to 500x500px + * @param {Object} options See defaults below. + */ + window.AjaxUpload = function(button, options){ + this._settings = { + // Location of the server-side upload script + action: 'upload.php', + // File upload name + name: 'userfile', + // Additional data to send + data: {}, + // Submit file as soon as it's selected + autoSubmit: true, + // The type of data that you're expecting back from the server. + // html and xml are detected automatically. + // Only useful when you are using json data as a response. + // Set to "json" in that case. + responseType: false, + // Class applied to button when mouse is hovered + hoverClass: 'hover', + // Class applied to button when button is focused + focusClass: 'focus', + // Class applied to button when AU is disabled + disabledClass: 'disabled', + // When user selects a file, useful with autoSubmit disabled + // You can return false to cancel upload + onChange: function(file, extension){ + }, + // Callback to fire before file is uploaded + // You can return false to cancel upload + onSubmit: function(file, extension){ + }, + // Fired when file upload is completed + // WARNING! DO NOT USE "FALSE" STRING AS A RESPONSE! + onComplete: function(file, response){ + } + }; + + // Merge the users options with our defaults + for (var i in options) { + if (options.hasOwnProperty(i)){ + this._settings[i] = options[i]; + } + } + + // button isn't necessary a dom element + if (button.jquery){ + // jQuery object was passed + button = button[0]; + } else if (typeof button == "string") { + if (/^#.*/.test(button)){ + // If jQuery user passes #elementId don't break it + button = button.slice(1); + } + + button = document.getElementById(button); + } + + if ( ! button || button.nodeType !== 1){ + throw new Error("Please make sure that you're passing a valid element"); + } + + if ( button.nodeName.toUpperCase() == 'A'){ + // disable link + addEvent(button, 'click', function(e){ + if (e && e.preventDefault){ + e.preventDefault(); + } else if (window.event){ + window.event.returnValue = false; + } + }); + } + + // DOM element + this._button = button; + // DOM element + this._input = null; + // If disabled clicking on button won't do anything + this._disabled = false; + + // if the button was disabled before refresh if will remain + // disabled in FireFox, let's fix it + this.enable(); + + this._rerouteClicks(); + }; + + // assigning methods to our class + AjaxUpload.prototype = { + setData: function(data){ + this._settings.data = data; + }, + disable: function(){ + addClass(this._button, this._settings.disabledClass); + this._disabled = true; + + var nodeName = this._button.nodeName.toUpperCase(); + if (nodeName == 'INPUT' || nodeName == 'BUTTON'){ + this._button.setAttribute('disabled', 'disabled'); + } + + // hide input + if (this._input){ + // We use visibility instead of display to fix problem with Safari 4 + // The problem is that the value of input doesn't change if it + // has display none when user selects a file + this._input.parentNode.style.visibility = 'hidden'; + } + }, + enable: function(){ + removeClass(this._button, this._settings.disabledClass); + this._button.removeAttribute('disabled'); + this._disabled = false; + + }, + /** + * Creates invisible file input + * that will hover above the button + * <div><input type='file' /></div> + */ + _createInput: function(){ + var self = this; + + var input = document.createElement("input"); + input.setAttribute('type', 'file'); + input.setAttribute('name', this._settings.name); + + addStyles(input, { + 'position' : 'absolute', + // in Opera only 'browse' button + // is clickable and it is located at + // the right side of the input + 'right' : 0, + 'margin' : 0, + 'padding' : 0, + 'fontSize' : '480px', + // in Firefox if font-family is set to + // 'inherit' the input doesn't work + 'fontFamily' : 'sans-serif', + 'cursor' : 'pointer' + }); + + var div = document.createElement("div"); + addStyles(div, { + 'display' : 'block', + 'position' : 'absolute', + 'overflow' : 'hidden', + 'margin' : 0, + 'padding' : 0, + 'opacity' : 0, + // Make sure browse button is in the right side + // in Internet Explorer + 'direction' : 'ltr', + //Max zIndex supported by Opera 9.0-9.2 + 'zIndex': 2147483583 + }); + + // Make sure that element opacity exists. + // Otherwise use IE filter + if ( div.style.opacity !== "0") { + if (typeof(div.filters) == 'undefined'){ + throw new Error('Opacity not supported by the browser'); + } + div.style.filter = "alpha(opacity=0)"; + } + + addEvent(input, 'change', function(){ + + if ( ! input || input.value === ''){ + return; + } + + // Get filename from input, required + // as some browsers have path instead of it + var file = fileFromPath(input.value); + + if (false === self._settings.onChange.call(self, file, getExt(file))){ + self._clearInput(); + return; + } + + // Submit form when value is changed + if (self._settings.autoSubmit) { + self.submit(); + } + }); + + addEvent(input, 'mouseover', function(){ + addClass(self._button, self._settings.hoverClass); + }); + + addEvent(input, 'mouseout', function(){ + removeClass(self._button, self._settings.hoverClass); + removeClass(self._button, self._settings.focusClass); + + // We use visibility instead of display to fix problem with Safari 4 + // The problem is that the value of input doesn't change if it + // has display none when user selects a file + input.parentNode.style.visibility = 'hidden'; + + }); + + addEvent(input, 'focus', function(){ + addClass(self._button, self._settings.focusClass); + }); + + addEvent(input, 'blur', function(){ + removeClass(self._button, self._settings.focusClass); + }); + + div.appendChild(input); + document.body.appendChild(div); + + this._input = input; + }, + _clearInput : function(){ + if (!this._input){ + return; + } + + // this._input.value = ''; Doesn't work in IE6 + removeNode(this._input.parentNode); + this._input = null; + this._createInput(); + + removeClass(this._button, this._settings.hoverClass); + removeClass(this._button, this._settings.focusClass); + }, + /** + * Function makes sure that when user clicks upload button, + * the this._input is clicked instead + */ + _rerouteClicks: function(){ + var self = this; + + // IE will later display 'access denied' error + // if you use using self._input.click() + // other browsers just ignore click() + + addEvent(self._button, 'mouseover', function(){ + if (self._disabled){ + return; + } + + if ( ! self._input){ + self._createInput(); + } + + var div = self._input.parentNode; + copyLayout(self._button, div); + div.style.visibility = 'visible'; + + }); + + + // commented because we now hide input on mouseleave + /** + * When the window is resized the elements + * can be misaligned if button position depends + * on window size + */ + //addResizeEvent(function(){ + // if (self._input){ + // copyLayout(self._button, self._input.parentNode); + // } + //}); + + }, + /** + * Creates iframe with unique name + * @return {Element} iframe + */ + _createIframe: function(){ + // We can't use getTime, because it sometimes return + // same value in safari :( + var id = getUID(); + + // We can't use following code as the name attribute + // won't be properly registered in IE6, and new window + // on form submit will open + // var iframe = document.createElement('iframe'); + // iframe.setAttribute('name', id); + + var iframe = toElement('<iframe src="javascript:false;" name="' + id + '" />'); + // src="javascript:false; was added + // because it possibly removes ie6 prompt + // "This page contains both secure and nonsecure items" + // Anyway, it doesn't do any harm. + iframe.setAttribute('id', id); + + iframe.style.display = 'none'; + document.body.appendChild(iframe); + + return iframe; + }, + /** + * Creates form, that will be submitted to iframe + * @param {Element} iframe Where to submit + * @return {Element} form + */ + _createForm: function(iframe){ + var settings = this._settings; + + // We can't use the following code in IE6 + // var form = document.createElement('form'); + // form.setAttribute('method', 'post'); + // form.setAttribute('enctype', 'multipart/form-data'); + // Because in this case file won't be attached to request + var form = toElement('<form method="post" enctype="multipart/form-data"></form>'); + + form.setAttribute('action', settings.action); + form.setAttribute('target', iframe.name); + form.style.display = 'none'; + document.body.appendChild(form); + + // Create hidden input element for each data key + for (var prop in settings.data) { + if (settings.data.hasOwnProperty(prop)){ + var el = document.createElement("input"); + el.setAttribute('type', 'hidden'); + el.setAttribute('name', prop); + el.setAttribute('value', settings.data[prop]); + form.appendChild(el); + } + } + return form; + }, + /** + * Gets response from iframe and fires onComplete event when ready + * @param iframe + * @param file Filename to use in onComplete callback + */ + _getResponse : function(iframe, file){ + // getting response + var toDeleteFlag = false, self = this, settings = this._settings; + + addEvent(iframe, 'load', function(){ + + if (// For Safari + iframe.src == "javascript:'%3Chtml%3E%3C/html%3E';" || + // For FF, IE + iframe.src == "javascript:'<html></html>';"){ + // First time around, do not delete. + // We reload to blank page, so that reloading main page + // does not re-submit the post. + + if (toDeleteFlag) { + // Fix busy state in FF3 + setTimeout(function(){ + removeNode(iframe); + }, 0); + } + + return; + } + + var doc = iframe.contentDocument ? iframe.contentDocument : window.frames[iframe.id].document; + + // fixing Opera 9.26,10.00 + if (doc.readyState && doc.readyState != 'complete') { + // Opera fires load event multiple times + // Even when the DOM is not ready yet + // this fix should not affect other browsers + return; + } + + // fixing Opera 9.64 + if (doc.body && doc.body.innerHTML == "false") { + // In Opera 9.64 event was fired second time + // when body.innerHTML changed from false + // to server response approx. after 1 sec + return; + } + + var response; + + if (doc.XMLDocument) { + // response is a xml document Internet Explorer property + response = doc.XMLDocument; + } else if (doc.body){ + // response is html document or plain text + response = doc.body.innerHTML; + + if (settings.responseType && settings.responseType.toLowerCase() == 'json') { + // If the document was sent as 'application/javascript' or + // 'text/javascript', then the browser wraps the text in a <pre> + // tag and performs html encoding on the contents. In this case, + // we need to pull the original text content from the text node's + // nodeValue property to retrieve the unmangled content. + // Note that IE6 only understands text/html + if (doc.body.firstChild && doc.body.firstChild.nodeName.toUpperCase() == 'PRE') { + doc.normalize(); + response = doc.body.firstChild.firstChild.nodeValue; + } + + if (response) { + response = eval("(" + response + ")"); + } else { + response = {}; + } + } + } else { + // response is a xml document + response = doc; + } + + settings.onComplete.call(self, file, response); + + // Reload blank page, so that reloading main page + // does not re-submit the post. Also, remember to + // delete the frame + toDeleteFlag = true; + + // Fix IE mixed content issue + iframe.src = "javascript:'<html></html>';"; + }); + }, + /** + * Upload file contained in this._input + */ + submit: function(){ + var self = this, settings = this._settings; + + if ( ! this._input || this._input.value === ''){ + return; + } + + var file = fileFromPath(this._input.value); + + // user returned false to cancel upload + if (false === settings.onSubmit.call(this, file, getExt(file))){ + this._clearInput(); + return; + } + + // sending request + var iframe = this._createIframe(); + var form = this._createForm(iframe); + + // assuming following structure + // div -> input type='file' + removeNode(this._input.parentNode); + removeClass(self._button, self._settings.hoverClass); + removeClass(self._button, self._settings.focusClass); + + form.appendChild(this._input); + + form.submit(); + + // request set, clean up + removeNode(form); form = null; + removeNode(this._input); this._input = null; + + // Get response from iframe and fire onComplete event when ready + this._getResponse(iframe, file); + + // get ready for next request + this._createInput(); + } + }; +})(); |