Commits

christiansalazar committed ada7352

welcome Coquito

  • Participants

Comments (0)

Files changed (13)

+<?php
+ /**
+ * CocoAction class file.
+ *
+ *
+ *	@example:
+ *		public function actions() { return array('coco'=>array('class'=>'CocoAction')); }
+ *
+ * @author Christian Salazar <christiansalazarh@gmail.com>
+ * @license http://opensource.org/licenses/bsd-license.php
+ */
+class CocoAction extends CAction {
+	/**
+	* this action invokes the appropiated handler referenced by a 'classname' url attribute.
+	* the specified classname must implements: EYuiActionRunnable.php
+	*/
+	public function run(){
+		//Yii::log('ACTION CALLED','info');
+		$inst = new CocoWidget();
+		$inst->runAction($_GET['action'],$_GET['data']);
+	}
+ }
+
+<?php
+/**
+	CocoWidget
+
+	This File uploader Tool is a Yii implementation for Valums File Uploader
+	(original source: http://github.com/valums/file-uploader)
+
+	@author	Christian Salazar (christiansalazarh@gmail.com) bluyell, @yiienespanol
+	@date oct-2012
+	@license http://opensource.org/licenses/bsd-license.php
+*/
+class CocoWidget extends CWidget implements EYuiActionRunnable {
+
+	public $id='cocowidget0';
+	public $debug;
+	public $htmlOptions;
+	public $defaultControllerName='site';
+	public $defaultActionName='coco';
+
+	public $buttonText='Find & Upload';
+	public $dropFilesText='Drop Files Here !';
+	public $allowedExtensions=array();
+	public $sizeLimit;
+	public $uploadDir = 'assets/';
+	public $onCompleted;
+	public $onCancelled;
+	public $onMessage;
+
+	private $_baseUrl;
+
+	public static function t($text){
+		return Yii::t("CocoWidget.coco",$text);
+	}
+
+	public function init(){
+		parent::init();
+		$this->registerCoreScripts();
+		if($this->sizeLimit == null)
+			$this->sizeLimit = 2 * 1024 * 1024;
+	}
+	public function run(){
+
+		$id=$this->id;
+		$upid = $id.'uploader';
+		$logid = $id.'logger';
+		$action = CHtml::normalizeUrl(array($this->defaultControllerName.'/'.$this->defaultActionName));
+
+		$htopts = '';
+		if(empty($this->htmlOptions)){
+			$htopts = "class='cocowidget'";
+		}else{
+			if(!isset($this->htmlOptions['class']))
+				$this->htmlOptions['class'] = 'cocowidget';
+			foreach($this->htmlOptions as $key=>$val)
+				$htopts .= " {$key}='$val'";
+		}
+
+		if($this->onCompleted == null)
+			$this->onCompleted = 'function(id,filename,jsoninfo){ }';
+		if(!($this->onCompleted instanceof CJavaScriptExpression))
+				$this->onCompleted = new CJavaScriptExpression($this->onCompleted);
+
+		if($this->onCancelled == null)
+			$this->onCancelled = 'function(id,filename){ }';
+		if(!($this->onCancelled instanceof CJavaScriptExpression))
+				$this->onCancelled = new CJavaScriptExpression($this->onCancelled);
+
+		if($this->onMessage == null)
+			$this->onMessage = 'function(messageText){ }';
+		if(!($this->onMessage instanceof CJavaScriptExpression))
+				$this->onMessage = new CJavaScriptExpression($this->onMessage);
+
+		$vars = array(
+			'allowedExtensions'=>$this->allowedExtensions,
+			'sizeLimit'=>$this->sizeLimit,
+			'uploadDir'=>$this->uploadDir,
+		);
+
+		$options = CJavaScript::encode(
+			array(
+				'id'=>$id,
+				'loggerid'=>$logid,
+				'action'=>$action,
+				'onCompleted'=>$this->onCompleted,
+				'onCancelled'=>$this->onCancelled,
+				'onMessage'=>$this->onMessage,
+				'buttonText'=>$this->buttonText,
+				'dropFilesText'=>$this->dropFilesText,
+				'uploaderContainer'=>$upid,
+				'data'=>serialize($vars),
+			)
+		);
+
+echo
+"
+	<!-- CocoWidget begins -->
+	<div id='{$id}' {$htopts}'>
+		<div id='{$upid}' class='uploader'></div>
+		<div id='{$logid}' class='logger'></div>
+	</div>
+	<!-- CocoWidget ends -->
+";
+
+		Yii::app()->getClientScript()->registerScript(
+			$id,
+"
+	var _cocoUp = new CocoWidget({$options});
+	_cocoUp.run();
+",
+			CClientScript::POS_LOAD
+		);
+	}
+
+	public function registerCoreScripts(){
+
+		$localAssetsDir = dirname(__FILE__) . '/assets';
+		$this->_baseUrl = Yii::app()->getAssetManager()->publish($localAssetsDir);
+
+        $cs = Yii::app()->getClientScript();
+        $cs->registerCoreScript('jquery');
+
+		if($this->debug)
+			$this->_baseUrl = 'protected/extensions/coco/assets';
+
+		foreach(scandir($localAssetsDir) as $f){
+			$_f = strtolower($f);
+			if(strstr($_f,".js"))
+				$cs->registerScriptFile($this->_baseUrl."/".$_f);
+			if(strstr($_f,".css"))
+				$cs->registerCssFile($this->_baseUrl."/".$_f);
+		}
+	}
+
+	// de: EYuiActionRunnable
+	public function runAction($action,$data) {
+		Yii::log('ACTION CALLED - action is: '.$action,'info');
+
+		$vars = unserialize($data);
+
+		$this->allowedExtensions = $vars['allowedExtensions'];
+		$this->sizeLimit = (integer)$vars['sizeLimit'];
+		$this->uploadDir = $vars['uploadDir'];
+
+		if(($this->allowedExtensions == null) || ($this->allowedExtensions==''))
+			$this->allowedExtensions = array();
+
+		Yii::log('ACTION CALLED - data is: '.CJSON::encode($vars),'info');
+
+
+		if($action == 'upload'){
+
+			$uploader = new ValumsFileUploader(array(), $this->sizeLimit);
+			if($uploader->checkServerSettings() != null){
+				Yii::log("CocoWidget. Please increase post_max_size and upload_max_filesize to ".$this->sizeLimit,"error");
+				return;
+			}
+
+			$result = $uploader->handleUpload($this->uploadDir);
+			if(isset($result['success'])){
+				if($result['success']==true){
+					Yii::log('ACTION CALLED - RESULT=SUCCESS','info');
+				}
+				else{
+					Yii::log('ACTION CALLED - RESULT=ERROR1','info');
+				}
+			}else
+			Yii::log('ACTION CALLED - RESULT=ERROR2','info');
+			echo htmlspecialchars(json_encode($result), ENT_NOQUOTES);
+		}
+
+	}
+}

EYuiActionRunnable.php

+<?php
+/**
+ * EYuiActionRunnable class file.
+ *
+ *	Allow every based EYuiWidget-Based widget to respond a EYuiAction call.
+ *
+ *	@see
+ *		EYuiAction
+ *
+ * @author Christian Salazar <christiansalazarh@gmail.com>
+ * @link https://bitbucket.org/christiansalazarh/eyui
+ * @license http://opensource.org/licenses/bsd-license.php
+ */
+interface EYuiActionRunnable {
+
+	/**
+	* Method called whenever an EYui widget is invoked from an action int order to start query model.
+	*
+	*/
+	public function runAction($action,$data);
+}
+Coco! Multi file Uploader Widget
+================================
+
+by: Christian Salazar. christiansalazarh@gmail.com	@yiienespanol, oct. 2012.
+
+(Español & English)
+
+1.	Single & Multi File Uploads via Ajax-jQuery
+1.	Drag & Drop.
+
+
+[EN]
+'Coco' is a widget for yii framework designed to handle File Uploads (Single and Multiple). Is designed using Ajax-jQuery and a well formed Architecture based on MVC (and UML).  Using 'coco' is very simple, at first place you setup a fixed action in any controller, this action serves for all every coco-widgets in your application. At second place you insert the widget in your form, all uploaded files will be stored in the path provided by 'uploadDir' widget attribute. Very simple and usefull. Please enjoy it.
+'
+Coco takes its functionality from a very nice PHP library located at valums repository in github, all licences are explicit in coco related to Valums work.
+
+
+[ES]
+'Coco' es un widget (aparte de ser el menor y mas temeroso de mis Yorkies) para manejar subidas de archivos a tu website. Con este widget se pretende crear una herramienta que te ayude a olvidarte de la complejidad de este asunto, sacando provecho de Yii Framework, jQuery, Ajax y UML/MVC
+
+La implementación del widget en tu proyecto es muy simple, se hace en dos partes: Primero, Pones el widget en la vista o formulario en donde lo requieras y Segundo en algun controller pones un action fijo en cualquier controller, no solamente aquel del formulario (un action fijo es aquel que se coloca en el metodo de actions() del controller), este action tiene como proposito conectar el código javascript del widget con tu aplicación Yii.
+
+Coco toma su funcionalidad de una librería PHP muy buena que consegui hace un año y que decidí implementar para Yii framework en forma de Widget. La licencia del autor de la libreria original es respetada y matiene sus creditos. Puedes hallarlo en el repositorio valums de github.
+
+
+INSTALACION / INSTALL
+---------------------
+
+1. 	GIT Cloning
+
+		cd /home/blabla/myapp/protected
+		mkdir extensions
+		cd extensions
+		git clone URL
+
+	[ES](Si no usas GIT simplemente copia el contenido de la extension directamente dentro de 'extensions')
+	[EN](If you dont use GIT please copy the entire 'coco' folder into your extensions folder)
+
+2.	Setup 'config/main'
+
+		'import'=>array(
+			'application.models.*',
+			'application.components.*',
+			'application.extensions.coco.*',			// <------
+		),
+
+3.	[ES]Conecta el widget con tu aplicación web, usa cualquier action (verifica que tu sistema RBAC le de permisos):
+	[EN]Connect the widget wit your current application using a fixed Action in siteController (or a distinct controller if you prefer).
+
+		[ES] edita
+		[EN] must edit:
+
+			myapp/protected/controllers/SiteController.php
+
+		[ES] agrega un action fijo
+		[EN] add a fixed action:
+
+		IMPORTANT:
+			Este action solo es requerido una vez para todo el proyecto !!
+			This action is required only one time for all above project !!
+
+			public function actions()
+			{
+				return array(
+					'captcha'=>array(
+						'class'=>'CCaptchaAction',
+						'backColor'=>0xFFFFFF,
+					),
+					'page'=>array(
+						'class'=>'CViewAction',
+					),
+					'coco'=>array(
+						'class'=>'CocoAction',
+					),
+				);
+			}
+
+
+4.	[ES] Usa el widget en cualquier form.
+	[EN] Use the widget in your form.
+
+		<?php
+			$this->widget('ext.coco.CocoWidget'
+				,array(
+					'id'=>'cocowidget1',
+
+					'onCompleted'=>'function(id,filename,jsoninfo){  }',
+					'onCancelled'=>'function(id,filename){ alert("cancelled "+filename); }',
+					'onMessage'=>'function(m){ alert(m); }',
+
+					'allowedExtensions'=>array('jpeg','jpg','gif','png'),
+					'sizeLimit'=>2000000,
+					'uploadDir' = 'assets/',
+				)
+			);
+		?>
+
+
+Extra Options
+-------------
+
+		'buttonText'=>'Find & Upload',
+		'dropFilesText'=>'Drop Files Here !',
+		'htmlOptions'=>array('style'=>'width: 300px; float: left; margin-right: 10px;'),
+		'defaultControllerName'=>'site',
+		'defaultActionName'=>'coco',
+

ValumsFileUploader.php

+<?php
+/**
+ * http://github.com/valums/file-uploader
+ *
+ * Multiple file upload component with progress-bar, drag-and-drop.
+ * © 2010 Andrew Valums ( andrew(at)valums.com )
+ *
+ * 	(Adapted to Yii Framework by:  Christian Salazar, christiansalazarh@gmail.com ,bluyell, @yiienespanol)
+ *
+ * Licensed under GNU GPL 2 or later and GNU LGPL 2 or later, see license.txt.
+ */
+class ValumsFileUploader {
+    private $allowedExtensions = array();
+    private $sizeLimit = 10485760;
+    private $file;
+
+    function __construct(array $allowedExtensions = array(), $sizeLimit = 10485760){
+        $allowedExtensions = array_map("strtolower", $allowedExtensions);
+
+        $this->allowedExtensions = $allowedExtensions;
+        $this->sizeLimit = $sizeLimit;
+
+        //$this->checkServerSettings();
+
+        if (isset($_GET['qqfile'])) {
+            $this->file = new qqUploadedFileXhr();
+        } elseif (isset($_FILES['qqfile'])) {
+            $this->file = new qqUploadedFileForm();
+        } else {
+            $this->file = false;
+        }
+    }
+
+    public function checkServerSettings(){
+        $postSize = $this->toBytes(ini_get('post_max_size'));
+        $uploadSize = $this->toBytes(ini_get('upload_max_filesize'));
+
+        if ($postSize < $this->sizeLimit || $uploadSize < $this->sizeLimit){
+            $size = max(1, $this->sizeLimit / 1024 / 1024) . 'M';
+            return array('error' => "increase post_max_size and upload_max_filesize to ".$size);
+        }
+
+        return null;
+    }
+
+    private function toBytes($str){
+        $val = trim($str);
+        $last = strtolower($str[strlen($str)-1]);
+        switch($last) {
+            case 'g': $val *= 1024;
+            case 'm': $val *= 1024;
+            case 'k': $val *= 1024;
+        }
+        return $val;
+    }
+
+    /**
+     * Returns array('success'=>true) or array('error'=>'error message')
+     */
+    function handleUpload($uploadDirectory, $replaceOldFile = FALSE){
+        if (!is_writable($uploadDirectory)){
+            return array('error' => CocoWidget::t("Server error. Upload directory isn't writable."));
+        }
+
+        if (!$this->file){
+            return array('error' => CocoWidget::t('No files were uploaded.'));
+        }
+
+        $size = $this->file->getSize();
+
+        if ($size == 0) {
+            return array('error' => CocoWidget::t('File is empty'));
+        }
+
+        if ($size > $this->sizeLimit) {
+            return array('error' => CocoWidget::t('File is too large'));
+        }
+
+        $pathinfo = pathinfo($this->file->getName());
+        $filename = $pathinfo['filename'];
+        //$filename = md5(uniqid());
+        $ext = $pathinfo['extension'];
+
+        if($this->allowedExtensions && !in_array(strtolower($ext), $this->allowedExtensions)){
+            $these = implode(', ', $this->allowedExtensions);
+            return array('error' => CocoWidget::t('File has an invalid extension, it should be one of '). $these . '.');
+        }
+
+        if(!$replaceOldFile){
+            /// don't overwrite previous files that were uploaded
+            while (file_exists($uploadDirectory . $filename . '.' . $ext)) {
+                $filename .= rand(10, 99);
+            }
+        }
+
+        $fullpath = $uploadDirectory . $filename . '.' . $ext;
+
+        if ($this->file->save($fullpath)){
+            return array('success'=>true,'filename'=>$filename,'size'=>$size,'ext'=>$ext,'path'=>$uploadDirectory,'fullpath'=>$fullpath);
+        } else {
+            return array('error'=>
+				CocoWidget::t('Could not save uploaded file. The upload was cancelled, or server error encountered'));
+        }
+
+    }
+}
+
+/*
+// list of valid extensions, ex. array("jpeg", "xml", "bmp")
+$allowedExtensions = array();
+// max file size in bytes
+$sizeLimit = 10 * 1024 * 1024;
+
+$uploader = new qqFileUploader($allowedExtensions, $sizeLimit);
+$result = $uploader->handleUpload('uploads/');
+// to pass data through iframe you will need to encode all html tags
+echo htmlspecialchars(json_encode($result), ENT_NOQUOTES);
+*/

assets/cocowidget.css

+.cocowidget {
+  border: 1px solid #eee;
+  padding: 3px;
+  overflow: auto;
+}
+.cocowidget .uploader {
+}
+cocowidget .logger {
+	display: none;
+}

assets/cocowidget.js

+var CocoWidget = function(options) {
+
+	this.nocache = function(){
+		var dateObject = new Date();
+		var uuid = dateObject.getTime();
+		return "&nocache="+uuid;
+	}
+
+	this.onComplete = function(id, fileName, responseJSON){
+		//$('#'+options.loggerid).append('<p>Completed:'+fileName+'</p>');
+		if(options.onCompleted != null)
+			options.onCompleted(id, fileName, responseJSON);
+
+	}
+
+	this.onCancel = function(id, fileName){
+		//$('#'+options.loggerid).append('<p>Cancelled: '+fileName+'</p>');
+		if(options.onCancelled != null)
+			options.onCancelled(id, fileName);
+	}
+
+	this.showMessage = function(messageText){
+		//$('#'+options.loggerid).append('<p>'+messageText+'</p>');
+		if(options.onMessage != null)
+			options.onMessage(messageText);
+
+	}
+
+	this.run = function(){
+		var _this = this;
+		var _uploader = new qq.FileUploader({
+			buttonText: options.buttonText,
+			dropFilesText: options.dropFilesText,
+			element: document.getElementById(options.uploaderContainer),
+			action: options.action + '&action=upload' + _this.nocache() + '&data='+options.data,
+			onComplete: _this.onComplete,
+			onCancel: _this.onCancel,
+			showMessage: _this.showMessage
+		});
+	}
+
+}

assets/fileuploader.css

+.qq-uploader { position:relative; width: 100%;}
+
+.qq-upload-button {
+    display:block; /* or inline-block */
+    width: 150px; padding: 7px 0; text-align:center;    
+    background:#0099CC; border-bottom:1px solid #ddd;color:#fff;
+}
+.qq-upload-button-hover {background:#10AABB;}
+.qq-upload-button-focus {outline:1px dotted black;}
+
+.qq-upload-drop-area {
+    position:absolute; top:0; left:0; width:100%; height:100%; min-height: 70px; z-index:2;
+    background:#FF9797; text-align:center; 
+}
+.qq-upload-drop-area span {
+    display:block; position:absolute; top: 50%; width:100%; margin-top:-8px; font-size:16px;
+}
+.qq-upload-drop-area-active {background:#FF7171;}
+
+.qq-upload-list {margin:15px 35px; padding:0; list-style:disc;}
+.qq-upload-list li { margin:0; padding:0; line-height:15px; font-size:12px;}
+.qq-upload-file, .qq-upload-spinner, .qq-upload-size, .qq-upload-cancel, .qq-upload-failed-text {
+    margin-right: 7px;
+}
+
+.qq-upload-file {}
+.qq-upload-spinner {display:inline-block; background: url("loading.gif"); width:15px; height:15px; vertical-align:text-bottom;}
+.qq-upload-size,.qq-upload-cancel {font-size:11px;}
+
+.qq-upload-failed-text {display:none;}
+.qq-upload-fail .qq-upload-failed-text {display:inline;}

assets/fileuploader.js

+/**
+ * http://github.com/valums/file-uploader
+ *
+ * Multiple file upload component with progress-bar, drag-and-drop.
+ * © 2010 Andrew Valums ( andrew(at)valums.com )
+ *
+ * Licensed under GNU GPL 2 or later and GNU LGPL 2 or later, see license.txt.
+ */
+
+//
+// Helper functions
+//
+
+var qq = qq || {};
+
+/**
+ * Adds all missing properties from second obj to first obj
+ */
+qq.extend = function(first, second){
+    for (var prop in second){
+        first[prop] = second[prop];
+    }
+};
+
+/**
+ * Searches for a given element in the array, returns -1 if it is not present.
+ * @param {Number} [from] The index at which to begin the search
+ */
+qq.indexOf = function(arr, elt, from){
+    if (arr.indexOf) return arr.indexOf(elt, from);
+
+    from = from || 0;
+    var len = arr.length;
+
+    if (from < 0) from += len;
+
+    for (; from < len; from++){
+        if (from in arr && arr[from] === elt){
+            return from;
+        }
+    }
+    return -1;
+};
+
+qq.getUniqueId = (function(){
+    var id = 0;
+    return function(){ return id++; };
+})();
+
+//
+// Events
+
+qq.attach = function(element, type, fn){
+    if (element.addEventListener){
+        element.addEventListener(type, fn, false);
+    } else if (element.attachEvent){
+        element.attachEvent('on' + type, fn);
+    }
+};
+qq.detach = function(element, type, fn){
+    if (element.removeEventListener){
+        element.removeEventListener(type, fn, false);
+    } else if (element.attachEvent){
+        element.detachEvent('on' + type, fn);
+    }
+};
+
+qq.preventDefault = function(e){
+    if (e.preventDefault){
+        e.preventDefault();
+    } else{
+        e.returnValue = false;
+    }
+};
+
+//
+// Node manipulations
+
+/**
+ * Insert node a before node b.
+ */
+qq.insertBefore = function(a, b){
+    b.parentNode.insertBefore(a, b);
+};
+qq.remove = function(element){
+    element.parentNode.removeChild(element);
+};
+
+qq.contains = function(parent, descendant){
+    // compareposition returns false in this case
+    if (parent == descendant) return true;
+
+    if (parent.contains){
+        return parent.contains(descendant);
+    } else {
+        return !!(descendant.compareDocumentPosition(parent) & 8);
+    }
+};
+
+/**
+ * Creates and returns element from html string
+ * Uses innerHTML to create an element
+ */
+qq.toElement = (function(){
+    var div = document.createElement('div');
+    return function(html){
+        div.innerHTML = html;
+        var element = div.firstChild;
+        div.removeChild(element);
+        return element;
+    };
+})();
+
+//
+// Node properties and attributes
+
+/**
+ * Sets styles for an element.
+ * Fixes opacity in IE6-8.
+ */
+qq.css = function(element, styles){
+    if (styles.opacity != null){
+        if (typeof element.style.opacity != 'string' && typeof(element.filters) != 'undefined'){
+            styles.filter = 'alpha(opacity=' + Math.round(100 * styles.opacity) + ')';
+        }
+    }
+    qq.extend(element.style, styles);
+};
+qq.hasClass = function(element, name){
+    var re = new RegExp('(^| )' + name + '( |$)');
+    return re.test(element.className);
+};
+qq.addClass = function(element, name){
+    if (!qq.hasClass(element, name)){
+        element.className += ' ' + name;
+    }
+};
+qq.removeClass = function(element, name){
+    var re = new RegExp('(^| )' + name + '( |$)');
+    element.className = element.className.replace(re, ' ').replace(/^\s+|\s+$/g, "");
+};
+qq.setText = function(element, text){
+    element.innerText = text;
+    element.textContent = text;
+};
+
+//
+// Selecting elements
+
+qq.children = function(element){
+    var children = [],
+    child = element.firstChild;
+
+    while (child){
+        if (child.nodeType == 1){
+            children.push(child);
+        }
+        child = child.nextSibling;
+    }
+
+    return children;
+};
+
+qq.getByClass = function(element, className){
+    if (element.querySelectorAll){
+        return element.querySelectorAll('.' + className);
+    }
+
+    var result = [];
+    var candidates = element.getElementsByTagName("*");
+    var len = candidates.length;
+
+    for (var i = 0; i < len; i++){
+        if (qq.hasClass(candidates[i], className)){
+            result.push(candidates[i]);
+        }
+    }
+    return result;
+};
+
+/**
+ * obj2url() takes a json-object as argument and generates
+ * a querystring. pretty much like jQuery.param()
+ *
+ * how to use:
+ *
+ *    `qq.obj2url({a:'b',c:'d'},'http://any.url/upload?otherParam=value');`
+ *
+ * will result in:
+ *
+ *    `http://any.url/upload?otherParam=value&a=b&c=d`
+ *
+ * @param  Object JSON-Object
+ * @param  String current querystring-part
+ * @return String encoded querystring
+ */
+qq.obj2url = function(obj, temp, prefixDone){
+    var uristrings = [],
+        prefix = '&',
+        add = function(nextObj, i){
+            var nextTemp = temp
+                ? (/\[\]$/.test(temp)) // prevent double-encoding
+                   ? temp
+                   : temp+'['+i+']'
+                : i;
+            if ((nextTemp != 'undefined') && (i != 'undefined')) {
+                uristrings.push(
+                    (typeof nextObj === 'object')
+                        ? qq.obj2url(nextObj, nextTemp, true)
+                        : (Object.prototype.toString.call(nextObj) === '[object Function]')
+                            ? encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj())
+                            : encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj)
+                );
+            }
+        };
+
+    if (!prefixDone && temp) {
+      prefix = (/\?/.test(temp)) ? (/\?$/.test(temp)) ? '' : '&' : '?';
+      uristrings.push(temp);
+      uristrings.push(qq.obj2url(obj));
+    } else if ((Object.prototype.toString.call(obj) === '[object Array]') && (typeof obj != 'undefined') ) {
+        // we wont use a for-in-loop on an array (performance)
+        for (var i = 0, len = obj.length; i < len; ++i){
+            add(obj[i], i);
+        }
+    } else if ((typeof obj != 'undefined') && (obj !== null) && (typeof obj === "object")){
+        // for anything else but a scalar, we will use for-in-loop
+        for (var i in obj){
+            add(obj[i], i);
+        }
+    } else {
+        uristrings.push(encodeURIComponent(temp) + '=' + encodeURIComponent(obj));
+    }
+
+    return uristrings.join(prefix)
+                     .replace(/^&/, '')
+                     .replace(/%20/g, '+');
+};
+
+//
+//
+// Uploader Classes
+//
+//
+
+var qq = qq || {};
+
+/**
+ * Creates upload button, validates upload, but doesn't create file list or dd.
+ */
+qq.FileUploaderBasic = function(o){
+    this._options = {
+        // set to true to see the server response
+        debug: false,
+        action: '/server/upload',
+        params: {},
+        button: null,
+        multiple: false,
+        maxConnections: 3,
+        // validation
+        allowedExtensions: [],
+        sizeLimit: 0,
+        minSizeLimit: 0,
+        // events
+        // return false to cancel submit
+        onSubmit: function(id, fileName){},
+        onProgress: function(id, fileName, loaded, total){},
+        onComplete: function(id, fileName, responseJSON){},
+        onCancel: function(id, fileName){},
+        // messages
+        messages: {
+            typeError: "{file} has invalid extension. Only {extensions} are allowed.",
+            sizeError: "{file} is too large, maximum file size is {sizeLimit}.",
+            minSizeError: "{file} is too small, minimum file size is {minSizeLimit}.",
+            emptyError: "{file} is empty, please select files again without it.",
+            onLeave: "The files are being uploaded, if you leave now the upload will be cancelled."
+        },
+        showMessage: function(message){
+            alert(message);
+        }
+    };
+    qq.extend(this._options, o);
+
+    // number of files being uploaded
+    this._filesInProgress = 0;
+    this._handler = this._createUploadHandler();
+
+    if (this._options.button){
+        this._button = this._createUploadButton(this._options.button);
+    }
+
+    this._preventLeaveInProgress();
+};
+
+qq.FileUploaderBasic.prototype = {
+    setParams: function(params){
+        this._options.params = params;
+    },
+    getInProgress: function(){
+        return this._filesInProgress;
+    },
+    _createUploadButton: function(element){
+        var self = this;
+
+        return new qq.UploadButton({
+            element: element,
+            multiple: this._options.multiple && qq.UploadHandlerXhr.isSupported(),
+            onChange: function(input){
+                self._onInputChange(input);
+            }
+        });
+    },
+    _createUploadHandler: function(){
+        var self = this,
+            handlerClass;
+
+        if(qq.UploadHandlerXhr.isSupported()){
+            handlerClass = 'UploadHandlerXhr';
+        } else {
+            handlerClass = 'UploadHandlerForm';
+        }
+
+        var handler = new qq[handlerClass]({
+            debug: this._options.debug,
+            action: this._options.action,
+            maxConnections: this._options.maxConnections,
+            onProgress: function(id, fileName, loaded, total){
+                self._onProgress(id, fileName, loaded, total);
+                self._options.onProgress(id, fileName, loaded, total);
+            },
+            onComplete: function(id, fileName, result){
+                self._onComplete(id, fileName, result);
+                self._options.onComplete(id, fileName, result);
+            },
+            onCancel: function(id, fileName){
+                self._onCancel(id, fileName);
+                self._options.onCancel(id, fileName);
+            }
+        });
+
+        return handler;
+    },
+    _preventLeaveInProgress: function(){
+        var self = this;
+
+        qq.attach(window, 'beforeunload', function(e){
+            if (!self._filesInProgress){return;}
+
+            var e = e || window.event;
+            // for ie, ff
+            e.returnValue = self._options.messages.onLeave;
+            // for webkit
+            return self._options.messages.onLeave;
+        });
+    },
+    _onSubmit: function(id, fileName){
+        this._filesInProgress++;
+    },
+    _onProgress: function(id, fileName, loaded, total){
+    },
+    _onComplete: function(id, fileName, result){
+        this._filesInProgress--;
+        if (result.error){
+            this._options.showMessage(result.error);
+        }
+    },
+    _onCancel: function(id, fileName){
+        this._filesInProgress--;
+    },
+    _onInputChange: function(input){
+        if (this._handler instanceof qq.UploadHandlerXhr){
+            this._uploadFileList(input.files);
+        } else {
+            if (this._validateFile(input)){
+                this._uploadFile(input);
+            }
+        }
+        this._button.reset();
+    },
+    _uploadFileList: function(files){
+        for (var i=0; i<files.length; i++){
+            if ( !this._validateFile(files[i])){
+                return;
+            }
+        }
+
+        for (var i=0; i<files.length; i++){
+            this._uploadFile(files[i]);
+        }
+    },
+    _uploadFile: function(fileContainer){
+        var id = this._handler.add(fileContainer);
+        var fileName = this._handler.getName(id);
+
+        if (this._options.onSubmit(id, fileName) !== false){
+            this._onSubmit(id, fileName);
+            this._handler.upload(id, this._options.params);
+        }
+    },
+    _validateFile: function(file){
+        var name, size;
+
+        if (file.value){
+            // it is a file input
+            // get input value and remove path to normalize
+            name = file.value.replace(/.*(\/|\\)/, "");
+        } else {
+            // fix missing properties in Safari
+            name = file.fileName != null ? file.fileName : file.name;
+            size = file.fileSize != null ? file.fileSize : file.size;
+        }
+
+        if (! this._isAllowedExtension(name)){
+            this._error('typeError', name);
+            return false;
+
+        } else if (size === 0){
+            this._error('emptyError', name);
+            return false;
+
+        } else if (size && this._options.sizeLimit && size > this._options.sizeLimit){
+            this._error('sizeError', name);
+            return false;
+
+        } else if (size && size < this._options.minSizeLimit){
+            this._error('minSizeError', name);
+            return false;
+        }
+
+        return true;
+    },
+    _error: function(code, fileName){
+        var message = this._options.messages[code];
+        function r(name, replacement){ message = message.replace(name, replacement); }
+
+        r('{file}', this._formatFileName(fileName));
+        r('{extensions}', this._options.allowedExtensions.join(', '));
+        r('{sizeLimit}', this._formatSize(this._options.sizeLimit));
+        r('{minSizeLimit}', this._formatSize(this._options.minSizeLimit));
+
+        this._options.showMessage(message);
+    },
+    _formatFileName: function(name){
+        if (name.length > 33){
+            name = name.slice(0, 19) + '...' + name.slice(-13);
+        }
+        return name;
+    },
+    _isAllowedExtension: function(fileName){
+        var ext = (-1 !== fileName.indexOf('.')) ? fileName.replace(/.*[.]/, '').toLowerCase() : '';
+        var allowed = this._options.allowedExtensions;
+
+        if (!allowed.length){return true;}
+
+        for (var i=0; i<allowed.length; i++){
+            if (allowed[i].toLowerCase() == ext){ return true;}
+        }
+
+        return false;
+    },
+    _formatSize: function(bytes){
+        var i = -1;
+        do {
+            bytes = bytes / 1024;
+            i++;
+        } while (bytes > 99);
+
+        return Math.max(bytes, 0.1).toFixed(1) + ['kB', 'MB', 'GB', 'TB', 'PB', 'EB'][i];
+    }
+};
+
+
+/**
+ * Class that creates upload widget with drag-and-drop and file list
+ * @inherits qq.FileUploaderBasic
+ */
+qq.FileUploader = function(o){
+    // call parent constructor
+    qq.FileUploaderBasic.apply(this, arguments);
+
+    // additional options
+    qq.extend(this._options, {
+        element: null,
+        // if set, will be used instead of qq-upload-list in template
+        listElement: null,
+
+        template: '<div class="qq-uploader">' +
+                '<div class="qq-upload-drop-area"><span>'+o.dropFilesText+'</span></div>' +
+                '<div class="qq-upload-button">'+o.buttonText+'</div>' +
+                '<ul class="qq-upload-list"></ul>' +
+             '</div>',
+
+        // template for one item in file list
+        fileTemplate: '<li>' +
+                '<span class="qq-upload-file"></span>' +
+                '<span class="qq-upload-spinner"></span>' +
+                '<span class="qq-upload-size"></span>' +
+                '<a class="qq-upload-cancel" href="#">Cancel</a>' +
+                '<span class="qq-upload-failed-text">Failed</span>' +
+            '</li>',
+
+        classes: {
+            // used to get elements from templates
+            button: 'qq-upload-button',
+            drop: 'qq-upload-drop-area',
+            dropActive: 'qq-upload-drop-area-active',
+            list: 'qq-upload-list',
+
+            file: 'qq-upload-file',
+            spinner: 'qq-upload-spinner',
+            size: 'qq-upload-size',
+            cancel: 'qq-upload-cancel',
+
+            // added to list item when upload completes
+            // used in css to hide progress spinner
+            success: 'qq-upload-success',
+            fail: 'qq-upload-fail'
+        }
+    });
+    // overwrite options with user supplied
+    qq.extend(this._options, o);
+
+    this._element = this._options.element;
+    this._element.innerHTML = this._options.template;
+    this._listElement = this._options.listElement || this._find(this._element, 'list');
+
+    this._classes = this._options.classes;
+
+    this._button = this._createUploadButton(this._find(this._element, 'button'));
+
+    this._bindCancelEvent();
+    this._setupDragDrop();
+};
+
+// inherit from Basic Uploader
+qq.extend(qq.FileUploader.prototype, qq.FileUploaderBasic.prototype);
+
+qq.extend(qq.FileUploader.prototype, {
+    /**
+     * Gets one of the elements listed in this._options.classes
+     **/
+    _find: function(parent, type){
+        var element = qq.getByClass(parent, this._options.classes[type])[0];
+        if (!element){
+            throw new Error('element not found ' + type);
+        }
+
+        return element;
+    },
+    _setupDragDrop: function(){
+        var self = this,
+            dropArea = this._find(this._element, 'drop');
+
+        var dz = new qq.UploadDropZone({
+            element: dropArea,
+            onEnter: function(e){
+                qq.addClass(dropArea, self._classes.dropActive);
+                e.stopPropagation();
+            },
+            onLeave: function(e){
+                e.stopPropagation();
+            },
+            onLeaveNotDescendants: function(e){
+                qq.removeClass(dropArea, self._classes.dropActive);
+            },
+            onDrop: function(e){
+                dropArea.style.display = 'none';
+                qq.removeClass(dropArea, self._classes.dropActive);
+                self._uploadFileList(e.dataTransfer.files);
+            }
+        });
+
+        dropArea.style.display = 'none';
+
+        qq.attach(document, 'dragenter', function(e){
+            if (!dz._isValidFileDrag(e)) return;
+
+            dropArea.style.display = 'block';
+        });
+        qq.attach(document, 'dragleave', function(e){
+            if (!dz._isValidFileDrag(e)) return;
+
+            var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);
+            // only fire when leaving document out
+            if ( ! relatedTarget || relatedTarget.nodeName == "HTML"){
+                dropArea.style.display = 'none';
+            }
+        });
+    },
+    _onSubmit: function(id, fileName){
+        qq.FileUploaderBasic.prototype._onSubmit.apply(this, arguments);
+        this._addToList(id, fileName);
+    },
+    _onProgress: function(id, fileName, loaded, total){
+        qq.FileUploaderBasic.prototype._onProgress.apply(this, arguments);
+
+        var item = this._getItemByFileId(id);
+        var size = this._find(item, 'size');
+        size.style.display = 'inline';
+
+        var text;
+        if (loaded != total){
+            text = Math.round(loaded / total * 100) + '% from ' + this._formatSize(total);
+        } else {
+            text = this._formatSize(total);
+        }
+
+        qq.setText(size, text);
+    },
+    _onComplete: function(id, fileName, result){
+        qq.FileUploaderBasic.prototype._onComplete.apply(this, arguments);
+
+        // mark completed
+        var item = this._getItemByFileId(id);
+        qq.remove(this._find(item, 'cancel'));
+        qq.remove(this._find(item, 'spinner'));
+
+        if (result.success){
+            qq.addClass(item, this._classes.success);
+        } else {
+            qq.addClass(item, this._classes.fail);
+        }
+    },
+    _addToList: function(id, fileName){
+        var item = qq.toElement(this._options.fileTemplate);
+        item.qqFileId = id;
+
+        var fileElement = this._find(item, 'file');
+        qq.setText(fileElement, this._formatFileName(fileName));
+        this._find(item, 'size').style.display = 'none';
+
+        this._listElement.appendChild(item);
+    },
+    _getItemByFileId: function(id){
+        var item = this._listElement.firstChild;
+
+        // there can't be txt nodes in dynamically created list
+        // and we can  use nextSibling
+        while (item){
+            if (item.qqFileId == id) return item;
+            item = item.nextSibling;
+        }
+    },
+    /**
+     * delegate click event for cancel link
+     **/
+    _bindCancelEvent: function(){
+        var self = this,
+            list = this._listElement;
+
+        qq.attach(list, 'click', function(e){
+            e = e || window.event;
+            var target = e.target || e.srcElement;
+
+            if (qq.hasClass(target, self._classes.cancel)){
+                qq.preventDefault(e);
+
+                var item = target.parentNode;
+                self._handler.cancel(item.qqFileId);
+                qq.remove(item);
+            }
+        });
+    }
+});
+
+qq.UploadDropZone = function(o){
+    this._options = {
+        element: null,
+        onEnter: function(e){},
+        onLeave: function(e){},
+        // is not fired when leaving element by hovering descendants
+        onLeaveNotDescendants: function(e){},
+        onDrop: function(e){}
+    };
+    qq.extend(this._options, o);
+
+    this._element = this._options.element;
+
+    this._disableDropOutside();
+    this._attachEvents();
+};
+
+qq.UploadDropZone.prototype = {
+    _disableDropOutside: function(e){
+        // run only once for all instances
+        if (!qq.UploadDropZone.dropOutsideDisabled ){
+
+            qq.attach(document, 'dragover', function(e){
+                if (e.dataTransfer){
+                    e.dataTransfer.dropEffect = 'none';
+                    e.preventDefault();
+                }
+            });
+
+            qq.UploadDropZone.dropOutsideDisabled = true;
+        }
+    },
+    _attachEvents: function(){
+        var self = this;
+
+        qq.attach(self._element, 'dragover', function(e){
+            if (!self._isValidFileDrag(e)) return;
+
+            var effect = e.dataTransfer.effectAllowed;
+            if (effect == 'move' || effect == 'linkMove'){
+                e.dataTransfer.dropEffect = 'move'; // for FF (only move allowed)
+            } else {
+                e.dataTransfer.dropEffect = 'copy'; // for Chrome
+            }
+
+            e.stopPropagation();
+            e.preventDefault();
+        });
+
+        qq.attach(self._element, 'dragenter', function(e){
+            if (!self._isValidFileDrag(e)) return;
+
+            self._options.onEnter(e);
+        });
+
+        qq.attach(self._element, 'dragleave', function(e){
+            if (!self._isValidFileDrag(e)) return;
+
+            self._options.onLeave(e);
+
+            var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);
+            // do not fire when moving a mouse over a descendant
+            if (qq.contains(this, relatedTarget)) return;
+
+            self._options.onLeaveNotDescendants(e);
+        });
+
+        qq.attach(self._element, 'drop', function(e){
+            if (!self._isValidFileDrag(e)) return;
+
+            e.preventDefault();
+            self._options.onDrop(e);
+        });
+    },
+    _isValidFileDrag: function(e){
+        var dt = e.dataTransfer,
+            // do not check dt.types.contains in webkit, because it crashes safari 4
+            isWebkit = navigator.userAgent.indexOf("AppleWebKit") > -1;
+
+        // dt.effectAllowed is none in Safari 5
+        // dt.types.contains check is for firefox
+        return dt && dt.effectAllowed != 'none' &&
+            (dt.files || (!isWebkit && dt.types.contains && dt.types.contains('Files')));
+
+    }
+};
+
+qq.UploadButton = function(o){
+    this._options = {
+        element: null,
+        // if set to true adds multiple attribute to file input
+        multiple: false,
+        // name attribute of file input
+        name: 'file',
+        onChange: function(input){},
+        hoverClass: 'qq-upload-button-hover',
+        focusClass: 'qq-upload-button-focus'
+    };
+
+    qq.extend(this._options, o);
+
+    this._element = this._options.element;
+
+    // make button suitable container for input
+    qq.css(this._element, {
+        position: 'relative',
+        overflow: 'hidden',
+        // Make sure browse button is in the right side
+        // in Internet Explorer
+        direction: 'ltr'
+    });
+
+    this._input = this._createInput();
+};
+
+qq.UploadButton.prototype = {
+    /* returns file input element */
+    getInput: function(){
+        return this._input;
+    },
+    /* cleans/recreates the file input */
+    reset: function(){
+        if (this._input.parentNode){
+            qq.remove(this._input);
+        }
+
+        qq.removeClass(this._element, this._options.focusClass);
+        this._input = this._createInput();
+    },
+    _createInput: function(){
+        var input = document.createElement("input");
+
+        if (this._options.multiple){
+            input.setAttribute("multiple", "multiple");
+        }
+
+        input.setAttribute("type", "file");
+        input.setAttribute("name", this._options.name);
+
+        qq.css(input, {
+            position: 'absolute',
+            // in Opera only 'browse' button
+            // is clickable and it is located at
+            // the right side of the input
+            right: 0,
+            top: 0,
+            fontFamily: 'Arial',
+            // 4 persons reported this, the max values that worked for them were 243, 236, 236, 118
+            fontSize: '118px',
+            margin: 0,
+            padding: 0,
+            cursor: 'pointer',
+            opacity: 0
+        });
+
+        this._element.appendChild(input);
+
+        var self = this;
+        qq.attach(input, 'change', function(){
+            self._options.onChange(input);
+        });
+
+        qq.attach(input, 'mouseover', function(){
+            qq.addClass(self._element, self._options.hoverClass);
+        });
+        qq.attach(input, 'mouseout', function(){
+            qq.removeClass(self._element, self._options.hoverClass);
+        });
+        qq.attach(input, 'focus', function(){
+            qq.addClass(self._element, self._options.focusClass);
+        });
+        qq.attach(input, 'blur', function(){
+            qq.removeClass(self._element, self._options.focusClass);
+        });
+
+        // IE and Opera, unfortunately have 2 tab stops on file input
+        // which is unacceptable in our case, disable keyboard access
+        if (window.attachEvent){
+            // it is IE or Opera
+            input.setAttribute('tabIndex', "-1");
+        }
+
+        return input;
+    }
+};
+
+/**
+ * Class for uploading files, uploading itself is handled by child classes
+ */
+qq.UploadHandlerAbstract = function(o){
+    this._options = {
+        debug: false,
+        action: '/upload.php',
+        // maximum number of concurrent uploads
+        maxConnections: 999,
+        onProgress: function(id, fileName, loaded, total){},
+        onComplete: function(id, fileName, response){},
+        onCancel: function(id, fileName){}
+    };
+    qq.extend(this._options, o);
+
+    this._queue = [];
+    // params for files in queue
+    this._params = [];
+};
+qq.UploadHandlerAbstract.prototype = {
+    log: function(str){
+        if (this._options.debug && window.console) console.log('[uploader] ' + str);
+    },
+    /**
+     * Adds file or file input to the queue
+     * @returns id
+     **/
+    add: function(file){},
+    /**
+     * Sends the file identified by id and additional query params to the server
+     */
+    upload: function(id, params){
+        var len = this._queue.push(id);
+
+        var copy = {};
+        qq.extend(copy, params);
+        this._params[id] = copy;
+
+        // if too many active uploads, wait...
+        if (len <= this._options.maxConnections){
+            this._upload(id, this._params[id]);
+        }
+    },
+    /**
+     * Cancels file upload by id
+     */
+    cancel: function(id){
+        this._cancel(id);
+        this._dequeue(id);
+    },
+    /**
+     * Cancells all uploads
+     */
+    cancelAll: function(){
+        for (var i=0; i<this._queue.length; i++){
+            this._cancel(this._queue[i]);
+        }
+        this._queue = [];
+    },
+    /**
+     * Returns name of the file identified by id
+     */
+    getName: function(id){},
+    /**
+     * Returns size of the file identified by id
+     */
+    getSize: function(id){},
+    /**
+     * Returns id of files being uploaded or
+     * waiting for their turn
+     */
+    getQueue: function(){
+        return this._queue;
+    },
+    /**
+     * Actual upload method
+     */
+    _upload: function(id){},
+    /**
+     * Actual cancel method
+     */
+    _cancel: function(id){},
+    /**
+     * Removes element from queue, starts upload of next
+     */
+    _dequeue: function(id){
+        var i = qq.indexOf(this._queue, id);
+        this._queue.splice(i, 1);
+
+        var max = this._options.maxConnections;
+
+        if (this._queue.length >= max && i < max){
+            var nextId = this._queue[max-1];
+            this._upload(nextId, this._params[nextId]);
+        }
+    }
+};
+
+/**
+ * Class for uploading files using form and iframe
+ * @inherits qq.UploadHandlerAbstract
+ */
+qq.UploadHandlerForm = function(o){
+    qq.UploadHandlerAbstract.apply(this, arguments);
+
+    this._inputs = {};
+};
+// @inherits qq.UploadHandlerAbstract
+qq.extend(qq.UploadHandlerForm.prototype, qq.UploadHandlerAbstract.prototype);
+
+qq.extend(qq.UploadHandlerForm.prototype, {
+    add: function(fileInput){
+        fileInput.setAttribute('name', 'qqfile');
+        var id = 'qq-upload-handler-iframe' + qq.getUniqueId();
+
+        this._inputs[id] = fileInput;
+
+        // remove file input from DOM
+        if (fileInput.parentNode){
+            qq.remove(fileInput);
+        }
+
+        return id;
+    },
+    getName: function(id){
+        // get input value and remove path to normalize
+        return this._inputs[id].value.replace(/.*(\/|\\)/, "");
+    },
+    _cancel: function(id){
+        this._options.onCancel(id, this.getName(id));
+
+        delete this._inputs[id];
+
+        var iframe = document.getElementById(id);
+        if (iframe){
+            // to cancel request set src to something else
+            // we use src="javascript:false;" because it doesn't
+            // trigger ie6 prompt on https
+            iframe.setAttribute('src', 'javascript:false;');
+
+            qq.remove(iframe);
+        }
+    },
+    _upload: function(id, params){
+        var input = this._inputs[id];
+
+        if (!input){
+            throw new Error('file with passed id was not added, or already uploaded or cancelled');
+        }
+
+        var fileName = this.getName(id);
+
+        var iframe = this._createIframe(id);
+        var form = this._createForm(iframe, params);
+        form.appendChild(input);
+
+        var self = this;
+        this._attachLoadEvent(iframe, function(){
+            self.log('iframe loaded');
+
+            var response = self._getIframeContentJSON(iframe);
+
+            self._options.onComplete(id, fileName, response);
+            self._dequeue(id);
+
+            delete self._inputs[id];
+            // timeout added to fix busy state in FF3.6
+            setTimeout(function(){
+                qq.remove(iframe);
+            }, 1);
+        });
+
+        form.submit();
+        qq.remove(form);
+
+        return id;
+    },
+    _attachLoadEvent: function(iframe, callback){
+        qq.attach(iframe, 'load', function(){
+            // when we remove iframe from dom
+            // the request stops, but in IE load
+            // event fires
+            if (!iframe.parentNode){
+                return;
+            }
+
+            // fixing Opera 10.53
+            if (iframe.contentDocument &&
+                iframe.contentDocument.body &&
+                iframe.contentDocument.body.innerHTML == "false"){
+                // In Opera event is fired second time
+                // when body.innerHTML changed from false
+                // to server response approx. after 1 sec
+                // when we upload file with iframe
+                return;
+            }
+
+            callback();
+        });
+    },
+    /**
+     * Returns json object received by iframe from server.
+     */
+    _getIframeContentJSON: function(iframe){
+        // iframe.contentWindow.document - for IE<7
+        var doc = iframe.contentDocument ? iframe.contentDocument: iframe.contentWindow.document,
+            response;
+
+        this.log("converting iframe's innerHTML to JSON");
+        this.log("innerHTML = " + doc.body.innerHTML);
+
+        try {
+            response = eval("(" + doc.body.innerHTML + ")");
+        } catch(err){
+            response = {};
+        }
+
+        return response;
+    },
+    /**
+     * Creates iframe with unique name
+     */
+    _createIframe: function(id){
+        // 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 = qq.toElement('<iframe src="javascript:false;" name="' + id + '" />');
+        // src="javascript:false;" removes ie6 prompt on https
+
+        iframe.setAttribute('id', id);
+
+        iframe.style.display = 'none';
+        document.body.appendChild(iframe);
+
+        return iframe;
+    },
+    /**
+     * Creates form, that will be submitted to iframe
+     */
+    _createForm: function(iframe, params){
+        // 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 = qq.toElement('<form method="post" enctype="multipart/form-data"></form>');
+
+        var queryString = qq.obj2url(params, this._options.action);
+
+        form.setAttribute('action', queryString);
+        form.setAttribute('target', iframe.name);
+        form.style.display = 'none';
+        document.body.appendChild(form);
+
+        return form;
+    }
+});
+
+/**
+ * Class for uploading files using xhr
+ * @inherits qq.UploadHandlerAbstract
+ */
+qq.UploadHandlerXhr = function(o){
+    qq.UploadHandlerAbstract.apply(this, arguments);
+
+    this._files = [];
+    this._xhrs = [];
+
+    // current loaded size in bytes for each file
+    this._loaded = [];
+};
+
+// static method
+qq.UploadHandlerXhr.isSupported = function(){
+    var input = document.createElement('input');
+    input.type = 'file';
+
+    return (
+        'multiple' in input &&
+        typeof File != "undefined" &&
+        typeof (new XMLHttpRequest()).upload != "undefined" );
+};
+
+// @inherits qq.UploadHandlerAbstract
+qq.extend(qq.UploadHandlerXhr.prototype, qq.UploadHandlerAbstract.prototype)
+
+qq.extend(qq.UploadHandlerXhr.prototype, {
+    /**
+     * Adds file to the queue
+     * Returns id to use with upload, cancel
+     **/
+    add: function(file){
+        if (!(file instanceof File)){
+            throw new Error('Passed obj in not a File (in qq.UploadHandlerXhr)');
+        }
+
+        return this._files.push(file) - 1;
+    },
+    getName: function(id){
+        var file = this._files[id];
+        // fix missing name in Safari 4
+        return file.fileName != null ? file.fileName : file.name;
+    },
+    getSize: function(id){
+        var file = this._files[id];
+        return file.fileSize != null ? file.fileSize : file.size;
+    },
+    /**
+     * Returns uploaded bytes for file identified by id
+     */
+    getLoaded: function(id){
+        return this._loaded[id] || 0;
+    },
+    /**
+     * Sends the file identified by id and additional query params to the server
+     * @param {Object} params name-value string pairs
+     */
+    _upload: function(id, params){
+        var file = this._files[id],
+            name = this.getName(id),
+            size = this.getSize(id);
+
+        this._loaded[id] = 0;
+
+        var xhr = this._xhrs[id] = new XMLHttpRequest();
+        var self = this;
+
+        xhr.upload.onprogress = function(e){
+            if (e.lengthComputable){
+                self._loaded[id] = e.loaded;
+                self._options.onProgress(id, name, e.loaded, e.total);
+            }
+        };
+
+        xhr.onreadystatechange = function(){
+            if (xhr.readyState == 4){
+                self._onComplete(id, xhr);
+            }
+        };
+
+        // build query string
+        params = params || {};
+        params['qqfile'] = name;
+        var queryString = qq.obj2url(params, this._options.action);
+
+        xhr.open("POST", queryString, true);
+        xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
+        xhr.setRequestHeader("X-File-Name", encodeURIComponent(name));
+        xhr.setRequestHeader("Content-Type", "application/octet-stream");
+        xhr.send(file);
+    },
+    _onComplete: function(id, xhr){
+        // the request was aborted/cancelled
+        if (!this._files[id]) return;
+
+        var name = this.getName(id);
+        var size = this.getSize(id);
+
+        this._options.onProgress(id, name, size, size);
+
+        if (xhr.status == 200){
+            this.log("xhr - server response received");
+            this.log("responseText = " + xhr.responseText);
+
+            var response;
+
+            try {
+                response = eval("(" + xhr.responseText + ")");
+            } catch(err){
+                response = {};
+            }
+
+            this._options.onComplete(id, name, response);
+
+        } else {
+            this._options.onComplete(id, name, {});
+        }
+
+        this._files[id] = null;
+        this._xhrs[id] = null;
+        this._dequeue(id);
+    },
+    _cancel: function(id){
+        this._options.onCancel(id, this.getName(id));
+
+        this._files[id] = null;
+
+        if (this._xhrs[id]){
+            this._xhrs[id].abort();
+            this._xhrs[id] = null;
+        }
+    }
+});

assets/loading.gif

Added
New image

messages/es/coco.php

+<?php
+	return array(
+		"Server error. Upload directory isn't writable."=>
+			'El directorio especificado para almacenar los archivos no tiene permisos de escritura.',
+		"No files were uploaded."=>
+			'No se subio ningun archivo',
+		"File is empty"=>
+			'El archivo esta vacio',
+		"File is too large"=>
+			'El archivo es muy grande',
+		"File has an invalid extension, it should be one of "=>
+			'El archivo tiene una extension no admitida. Debe ser una de: ',
+		"Could not save uploaded file. The upload was cancelled, or server error encountered"=>
+			'No se pudo guardar el archivo. Este fue cancelado o el servidor encontro un error',
+	);
+?>

qqUploadedFileForm.php

+<?php
+/**
+ * Handle file uploads via regular form post (uses the $_FILES array)
+ */
+class qqUploadedFileForm {
+    /**
+     * Save the file to the specified path
+     * @return boolean TRUE on success
+     */
+    function save($path) {
+        if(!move_uploaded_file($_FILES['qqfile']['tmp_name'], $path)){
+            return false;
+        }
+        return true;
+    }
+    function getName() {
+        return $_FILES['qqfile']['name'];
+    }
+    function getSize() {
+        return $_FILES['qqfile']['size'];
+    }
+}

qqUploadedFileXhr.php

+<?php
+/**
+ * Handle file uploads via XMLHttpRequest
+ */
+class qqUploadedFileXhr {
+    /**
+     * Save the file to the specified path
+     * @return boolean TRUE on success
+     */
+    function save($path) {
+        $input = fopen("php://input", "r");
+        $temp = tmpfile();
+        $realSize = stream_copy_to_stream($input, $temp);
+        fclose($input);
+
+        if ($realSize != $this->getSize()){
+            return false;
+        }
+
+        $target = fopen($path, "w");
+        fseek($temp, 0, SEEK_SET);
+        stream_copy_to_stream($temp, $target);
+        fclose($target);
+
+        return true;
+    }
+    function getName() {
+        return $_GET['qqfile'];
+    }
+    function getSize() {
+        if (isset($_SERVER["CONTENT_LENGTH"])){
+            return (int)$_SERVER["CONTENT_LENGTH"];
+        } else {
+            throw new Exception('Getting content length is not supported.');
+        }
+    }
+}