Tuesday, March 24, 2009

Trinidad Draggable Dialogs

Users of Apache Trinidad may have run across the issue of not being able to move the Trinidad dialogs around the page. They always sit right in the center of your screen and of course block you from seeing the information behind. In release 1.02 the authors proposed that the dialogs should be made drag-able, however nothing has been done. The following is a simple solution I used to make the dialogs drag-able. If I had time, I would commit something to the project to handle this - although I believe the reason it has not yet been implemented is because of the amount of cross browser code needed to affect this. The following solution took about 5 hours to come up with and refine.

The trick with making the dialogs drag-able is that that the dialogs are generated after the page loads, use different markup depending on browser type and version, and the markup contains no ids on the DOM elements of interest. I implemented this for Firefox2, 3 and ie7. For other browsers, the dialog is rendered with standard behavior.


So lets get started:

Step 1: Add the mouse drag event script to the Trinidad dialog:

In your xhtml/jsp file that will display behind the dialog , add a trh:script call that is rendered conditionally based on browser. The call will need to wait until the page loads to execute, so I add a timer to delay invokation:

trh:script text="window.setTimeout('attachDraggabilityToDialog()', 500);"
rendered="#{UiUtils.supportsDialogDrag}"

partialTriggers="::pprDialogCallButton "


The text attribute calls the attachDraggabilityToDialog script 500 ms after the page loads ( I place the script at the bottom of the page. )

The rendered attribute calls a backing bean function that specifies whether the browser version is supported for dragging.

The Partial Triggers attribute references Trinidad element id that pops up the dialog - in this case the button.

Step 2. Create the script:

// the frame
var dialog_frameDiv= null;
// the title bar on the frame
var dialog_titleBar=null;
// the object to drag around
var dialog_dragObj = new Object();
var GECKO=0;
var IE=1;
var BR_VER=GECKO;
new Browser();




/**
* After the page has loaded, and the popup has been displayed,
* Call this function:
* e.g: window.setTimeout(attachDraggabilityToDialog, 500);
* NOTE _ WORKS IN FF2, 3 AND IE 7
*
*/
function attachDraggabilityToDialog() {
dialog_dragObj = new Object();
dialog_frameDiv= null;
dialog_titleBar=null;

if (BR_VER==GECKO) {
for(var i =0; i <
window.document.body.childNodes.length;i++) {
dialog_frameDiv= window.document.body.childNodes[i];
if(dialog_frameDiv!=null && dialog_frameDiv.nodeName=="DIV" && dialog_frameDiv.style.zIndex==5001) {
dialog_titleBar=dialog_frameDiv.childNodes[0];
break;
}
}
}
else if (BR_VER==IE) {
var _node = document.getElementsByTagName("iframe")[0].parentNode;
if(_node !=null && _node.tagName=="DIV"){
dialog_titleBar= dialog_frameDiv=_node;
}
}

if(dialog_titleBar !=null) {
if (BR_VER==IE) {
dialog_titleBar.attachEvent("onmousedown", callDrag);
dialog_titleBar.attachEvent("onmouseup", endDrag);
}
if (BR_VER==GECKO) {
dialog_titleBar.addEventListener("mousedown", callDrag, true);
dialog_titleBar.addEventListener("mouseup", endDrag, true);
}
}

}



Discussion: Based on browser version, we look through the generated markup in the page once the dialog has appeared and find the element based on attributes or expected location of the node in the tree. While not foolproof, this technique works in the many scenarios in our present application, as Trinidad generates the dialogs in a standard manner per browser and version.

The attachDraggabilityToScript injects event handlers onto the elements to be dragged. The event handlers are below:


function callDrag(event) {

var el;
var x, y;
dialog_dragObj.elNode = dialog_frameDiv;

if (BR_VER==IE) {
x = window.event.clientX + document.documentElement.scrollLeft + document.body.scrollLeft;
y = window.event.clientY + document.documentElement.scrollTop+ document.body.scrollTop;
}
if (BR_VER==GECKO) {
x = event.clientX + window.scrollX;
y = event.clientY + window.scrollY;
}


dialog_dragObj.cursorStartX = x;
dialog_dragObj.cursorStartY = y;
dialog_dragObj.elStartLeft = parseInt(dialog_dragObj.elNode.style.left, 10);
dialog_dragObj.elStartTop = parseInt(dialog_dragObj.elNode.style.top, 10);

if (isNaN(dialog_dragObj.elStartLeft)) dialog_dragObj.elStartLeft = 0;
if (isNaN(dialog_dragObj.elStartTop)) dialog_dragObj.elStartTop = 0;

if (BR_VER==IE) {
document.attachEvent("onmousemove", startDrag);
document.attachEvent("onmouseup", endDrag);
window.event.cancelBubble = true;
window.event.returnValue = false;
}
if (BR_VER==GECKO) {
document.addEventListener("mousemove", startDrag, true);
document.addEventListener("mouseup", endDrag, true);
event.preventDefault();
}

}


function startDrag(event) {

var x, y;


if (BR_VER==IE) {
x = window.event.clientX + document.documentElement.scrollLeft + document.body.scrollLeft;
y = window.event.clientY + document.documentElement.scrollTop+ document.body.scrollTop;
}
if (BR_VER==GECKO) {
x = event.clientX + window.scrollX;
y = event.clientY + window.scrollY;
}


// Move drag element by the same amount the cursor has moved.

dialog_dragObj.elNode.style.left =(dialog_dragObj.elStartLeft + x - dialog_dragObj.cursorStartX) + "px";
dialog_dragObj.elNode.style.top =(dialog_dragObj.elStartTop + y - dialog_dragObj.cursorStartY) + "px";

if (BR_VER==IE) {
window.event.cancelBubble = true;
window.event.returnValue = false;
}
if (BR_VER==GECKO) ;
event.preventDefault();
}


function endDrag(event) {
if (BR_VER==IE) {
document.detachEvent("onmousemove", startDrag);
document.detachEvent("onmouseup", stopDrag);
}
if (BR_VER==GECKO) {
document.removeEventListener("mousemove", startDrag, true);
document.removeEventListener("mouseup", stopDrag, true);
}
}



Discussion : The above event handlers move the DOM element based on mouse movements after the user mousesdown on the element, and releases once the user mouses up based on browser version.


Finally, I have a simple browser detect , knowing that my script will not be called unless the browser is Firefox2, 3 or IE 7.


function Browser() {
var ua, i;
ua = navigator.userAgent;
if ((i=ua.indexOf("MSIE")) >= 0) {
BR_VER=IE;
}
}


In my backing bean, I have the following browser detection code that conditionally renders the call to the script based on its findings:



/**
* Currently only ff2,3 ie 7
* @return tru if the browser is one of the above.
*/
public boolean getSupportsDialogDrag() {
String ua = UiBaseUtils.getRequest().getHeader("User-Agent");

if(ua ==null)
return false;

if(ua.contains("MSIE 7"))
return true;
if(ua.toUpperCase().contains("GECKO")){
if(ua.contains("Firefox/3."))
return true;
if(ua.contains("Firefox/2."))
return true;
}
return false;

}



That's it.

I believe the correct way to do this would be to donate time and code to the Trinidad Project. I hope to do this , but I needed to come up with a quick solution that worked. I was able to commit this in less than 24 hours after getting the requirement, so please take this solution with this caveat in mind. QA has found no issues, and user feedback has been positive.

No comments: