I finally couldn't avoid the need for AJAX file upload functionality, so I spent a few hours today modifying Qcodo to allow that. I was actually able to achieve success without even using any outside libraries or having to modify QFileControl! I've tested it successfully in both IE7 and Fx2, though it should work in other browsers too. Here's what you'll need to patch (to 0.3.21, though I don't think the affected areas changed much in 0.3.24):
assets/js/_core/post.js (add/replace the following methods):
qcodo.postAjax = function(strForm, strControl, strEvent, strParameter, strWaitIconControlId, blnUploadFile) {
// Figure out if Queue is Empty
var blnQueueEmpty = false;
if (qcodo.ajaxQueue.length == 0)
blnQueueEmpty = true;
// Enqueue the AJAX Request
qcodo.ajaxQueue.push(new Array(strForm, strControl, strEvent, strParameter, strWaitIconControlId, blnUploadFile));
// If the Queue was originally empty, call the Dequeue
if (blnQueueEmpty)
qcodo.dequeueAjaxQueue();
};
...
qcodo.dequeueAjaxQueue = function() {
if (qcodo.ajaxQueue.length > 0) {
strForm = this.ajaxQueue[0][0];
strControl = this.ajaxQueue[0][1];
strEvent = this.ajaxQueue[0][2];
strParameter = this.ajaxQueue[0][3];
strWaitIconControlId = this.ajaxQueue[0][4];
blnUploadFile = this.ajaxQueue[0][5];
// Display WaitIcon (if applicable)
if (strWaitIconControlId) {
this.objAjaxWaitIcon = this.getWrapper(strWaitIconControlId);
if (this.objAjaxWaitIcon)
this.objAjaxWaitIcon.style.display = 'inline';
};
var objForm = document.getElementById(strForm);
objForm.Qform__FormControl.value = strControl;
objForm.Qform__FormEvent.value = strEvent;
objForm.Qform__FormParameter.value = strParameter;
objForm.Qform__FormCallType.value = "Ajax";
objForm.Qform__FormUpdates.value = qcodo.formUpdates();
if(blnUploadFile) {
objForm.Qform__FormCheckableControls.value = this.formCheckableControls(strForm, "Server");
var id = objForm.id + "_iframe";
var d = document.createElement('DIV');
d.innerHTML = '<iframe style="display:none" src="about:blank" id="'+id+'" name="'+id+'" onload="qcodo.onIframeLoad(this);"></iframe>';
document.body.appendChild(d);
objForm.target = id;
objForm.submit();
return;
} else
objForm.Qform__FormCheckableControls.value = this.formCheckableControls(strForm, "Ajax");
var strPostData = "";
for (var i = 0; i < objForm.elements.length; i++) {
switch (objForm.elements[i].type) {
case "checkbox":
case "radio":
if (objForm.elements[i].checked) {
var strTestName = objForm.elements[i].name + "_";
if (objForm.elements[i].id.substring(0, strTestName.length) == strTestName)
strPostData += "&" + objForm.elements[i].name + "=" + objForm.elements[i].id.substring(strTestName.length);
else
strPostData += "&" + objForm.elements[i].id + "=" + "1";
};
break;
case "select-multiple":
var blnOneSelected = false;
for (var intIndex = 0; intIndex < objForm.elements[i].options.length; intIndex++)
if (objForm.elements[i].options[intIndex].selected) {
strPostData += "&" + objForm.elements[i].name + "=";
strPostData += objForm.elements[i].options[intIndex].value;
};
break;
default:
strPostData += "&" + objForm.elements[i].id + "=";
// For Internationalization -- we must escape the element's value properly
var strPostValue = objForm.elements[i].value;
if (strPostValue) {
strPostValue = strPostValue.replace(/&/g, escape('&'));
strPostValue = strPostValue.replace(/\+/g, "+");
};
strPostData += strPostValue;
break;
};
};
var strUri = objForm.action;
var objRequest;
if (window.XMLHttpRequest) {
objRequest = new XMLHttpRequest();
} else if (typeof ActiveXObject != "undefined") {
objRequest = new ActiveXObject("Microsoft.XMLHTTP");
};
if (objRequest) {
objRequest.open("POST", strUri, true);
objRequest.setRequestHeader("Method", "POST " + strUri + " HTTP/1.1");
objRequest.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
objRequest.onreadystatechange = function() {
if (objRequest.readyState == 4) {
qcodo.processResponse(objRequest.responseText, objRequest.responseXML);
};
};
objRequest.send(strPostData);
};
};
};
qcodo.onIframeLoad = function(frame) {
try {
var doc;
if(frame.contentDocument)
doc = frame.contentDocument;
else if(frame.contentWindow)
doc = frame.contentWindow.document;
else
doc = window.frames[frame.id].document;
if(doc.location.href == "about:blank")
return;
var responseText, responseXML;
if(doc && doc.firstChild)
responseText = doc.firstChild.innerHTML;
if(doc && doc.XMLDocument)
responseXML = doc.XMLDocument;
else
responseXML = doc;
qcodo.processResponse(responseText, responseXML);
} catch(e) {
alert("An error occurred during AJAX IFRAME Response parsing.");
};
setTimeout(function(){document.body.removeChild(frame.parentNode);}, 100);
};
qcodo.processResponse = function(responseText, objXmlDoc) {
try {
// qcodo.logMessage(objRequest.responseText, true);
// alert('AJAX Response Received');
if ((!objXmlDoc) || (!objXmlDoc.documentElement) || (objXmlDoc.getElementsByTagName('control').length == 0)) {
alert("An error occurred during AJAX Response parsing.\r\n\r\nThe error response will appear in a new popup.");
var objErrorWindow = window.open('about:blank', 'qcodo_error','menubar=no,toolbar=no,location=no,status=no,scrollbars=yes,resizable=yes,width=1000,height=700,left=50,top=50');
objErrorWindow.focus();
objErrorWindow.document.write(responseText);
return;
} else {
var intLength = 0;
// Go through Controls
var objXmlControls = objXmlDoc.getElementsByTagName('control');
intLength = objXmlControls.length;
for (var intIndex = 0; intIndex < intLength; intIndex++) {
var strControlId = objXmlControls[intIndex].attributes.getNamedItem('id').nodeValue;
var strControlHtml = "";
if (objXmlControls[intIndex].firstChild)
strControlHtml = objXmlControls[intIndex].firstChild.nodeValue;
if (qcodo.isBrowser(qcodo.FIREFOX))
strControlHtml = objXmlControls[intIndex].textContent;
// Perform Callback Responsibility
if (strControlId == "Qform__FormState") {
var objFormState = document.getElementById(strControlId);
objFormState.value = strControlHtml;
} else {
var objSpan = document.getElementById(strControlId + "_ctl");
if (objSpan)
objSpan.innerHTML = strControlHtml;
};
};
// Go through Commands
var objXmlCommands = objXmlDoc.getElementsByTagName('command');
intLength = objXmlCommands.length;
for (var intIndex = 0; intIndex < intLength; intIndex++) {
if (objXmlCommands[intIndex] && objXmlCommands[intIndex].firstChild) {
var strCommand = "";
intChildLength = objXmlCommands[intIndex].childNodes.length;
for (var intChildIndex = 0; intChildIndex < intChildLength; intChildIndex++)
strCommand += objXmlCommands[intIndex].childNodes[intChildIndex].nodeValue;
eval(strCommand);
};
};
};
} catch (objExc) {
alert(objExc.message + "\r\non line number " + objExc.lineNumber + "\r\nin file " + objExc.fileName);
alert("An error occurred during AJAX Response handling.\r\n\r\nThe error response will appear in a new popup.");
var objErrorWindow = window.open('about:blank', 'qcodo_error','menubar=no,toolbar=no,location=no,status=no,scrollbars=yes,resizable=yes,width=1000,height=700,left=50,top=50');
objErrorWindow.focus();
objErrorWindow.document.write(responseText);
return;
};
// Perform the Dequeue
qcodo.ajaxQueue.reverse();
qcodo.ajaxQueue.pop();
qcodo.ajaxQueue.reverse();
// Hid the WaitIcon (if applicable)
if (qcodo.objAjaxWaitIcon)
qcodo.objAjaxWaitIcon.style.display = 'none';
// Clean up after any elements that got removed
for (var strGroupingId in qcodo.dropZoneGrouping)
for (var strControlId in qcodo.dropZoneGrouping[strGroupingId])
if(!document.getElementById(strControlId))
qcodo.dropZoneGrouping[strGroupingId][strControlId] = false;
// If there are still AjaxEvents in the queue, go ahead and process/dequeue them
if (qcodo.ajaxQueue.length > 0)
qcodo.dequeueAjaxQueue();
};
includes/qcodo/_core/qform/_actions.inc.php (replace the following classes; ParamOverride is an unrelated modification of mine):
class QAjaxAction extends QAction {
protected $strMethodName;
protected $objWaitIconControl;
protected $strParamOverride;
protected $blnUploadFile;
public function __construct($strMethodName = null, $objWaitIconControl = 'default', $strParamOverride = null, $blnUploadFile = false) {
$this->strMethodName = $strMethodName;
$this->objWaitIconControl = $objWaitIconControl;
$this->strParamOverride = $strParamOverride;
$this->blnUploadFile = $blnUploadFile;
}
public function __get($strName) {
switch ($strName) {
case 'MethodName':
return $this->strMethodName;
case 'WaitIconControl':
return $this->objWaitIconControl;
default:
try {
return parent::__get($strName);
} catch (QCallerException $objExc) {
$objExc->IncrementOffset();
throw $objExc;
}
}
}
public function RenderScript(QControl $objControl) {
$strWaitIconControlId = null;
if ((gettype($this->objWaitIconControl) == 'string') && ($this->objWaitIconControl == 'default')) {
if ($objControl->Form->DefaultWaitIcon)
$strWaitIconControlId = $objControl->Form->DefaultWaitIcon->ControlId;
} else if ($this->objWaitIconControl) {
$strWaitIconControlId = $this->objWaitIconControl->ControlId;
}
return sprintf("qc.pA('%s', '%s', '%s', %s, '%s', %s);",
$objControl->Form->FormId, $objControl->ControlId, get_class($this->objEvent), $this->strParamOverride !== null ? $this->strParamOverride : ("'" . addslashes($objControl->ActionParameter) . "'"), $strWaitIconControlId, $this->blnUploadFile ? 'true' : 'false');
}
}
class QAjaxControlAction extends QAjaxAction {
public function __construct(QControl $objControl, $strMethodName, $objWaitIconControl = 'default', $strParamOverride = null, $blnUploadFile = false) {
parent::__construct($objControl->ControlId . ':' . $strMethodName, $objWaitIconControl, $strParamOverride, $blnUploadFile);
}
}
includes/qcodo/_core/qform/QRadioButton.class.php (replace the following method; I removed the apparently unnecessary QRequestMode::Ajax check which was causing problems with my hybrid post/ajax request):
public function ParsePostData() {
if ($this->objForm->IsCheckableControlRendered($this->strControlId)) {
if ($this->strGroupName) {
if(array_key_exists($this->strGroupName, $_POST))
$this->blnChecked = $_POST[$this->strGroupName] == $this->strControlId;
} else
$this->blnChecked = array_key_exists($this->strControlId, $_POST) && $_POST[$this->strControlId];
}
}
Once the patches are applied, all you need to do to AJAXify file uploading is add a true to the appropriate place in the QAjaxAction constructor of the button that you want to use to trigger the file upload. That's it!
I'd appreciate testers and feedback. If this works well for everyone, it'd be really nice to have put into the core.