Friday, January 8, 2010

JSFBlueprint auto-suggest field enhancements

What follows is an listing of the javascript that goes with the auto-suggest field you can find in JSF Blueprints.

The original brought up a basic list of suggestions which could be mouse selected. I added code to allow you to arrow down and up directly if you did not want to reach for your mouse. Also I added code (per a suggestion by Frank Nimphius, Oracle Corporation), which would reduce the number of ajax calls...very important for many use-cases of this component.

gReq = "";
var req;
var requests;
var target;

function getElementX(element){
var targetLeft = 0;
while (element) {
if (element.offsetParent) {
targetLeft += element.offsetLeft;
} else if (element.x) {
targetLeft += element.x;
}
element = element.offsetParent;
}
return targetLeft;
}


function getElementY(element){
var targetTop = 0;
while (element) {
if (element.offsetParent) {
targetTop += element.offsetTop;
} else if (element.y) {
targetTop += element.y;
}
element = element.offsetParent;
}
return targetTop;
}

function getWidth(element){
if (element.clientWidth && element.offsetWidth && element.clientWidth < element.offsetWidth) {
return element.clientWidth; /* some mozillas (like 1.4.1) return bogus clientWidth so ensure it's in range */
} else if (element.offsetWidth) {
return element.offsetWidth;
} else if (element.width) {
return element.width;
} else {
return 0;
}
}

function initRequest(url) {
if (window.XMLHttpRequest) {
req = new XMLHttpRequest();
} else if (window.ActiveXObject) {
req = new ActiveXObject("Microsoft.XMLHTTP");
}

// ********** INSERTED CODE ***********************
// The general idea here is that, the end user is only interested in the
// latest request. As of right now (9/20/2009), I have not seen anything
// to indicate otherwise. Assuming this is true, I am hoping that
// we never have a corresponding backlog of request on the database end.
if (typeof(gReq) != 'undefined' && typeof(gReq.readyState) != 'undefined'
&& typeof(req) != 'undefined' && typeof(req.readyState) != 'undefined') {
// alert("gReq.readyState is " + gReq.readyState + ", and req.readyState is " +
// req.readyState);
gReq.abort();
}
gReq = req;
//1/1/2010, mfons, The above was a brilliant idea, but it did nothing I can discern: still
// querying every keystroke. So now I
// will try waiting .5 seconds before registering the keystroke...if
// they are still typing I will keep waiting until I get something
// that has not changed...then query. See doCompletionDelayed()
// function below.
// ************ END OF INSERTED CODE **************
}

/**
* 12/31/09, mfons - originally doCompletion() called the ajax for each keyclick,
* but really we only want to only call the db if the user has not typed anything new
* in a while. Credit for this idea goes to Frank Nimphius, Oracle Corp.,
* who responded to my query on the technet.oracle.com jdeveloper forum.
* However, I figured the coding out myself.
**/
function doCompletionDelayed(targetName, menuName, method, onchoose, ondisplay, oldTargetValue) {
var target = document.getElementById(targetName);
if (target.value == oldTargetValue) {
var menu = document.getElementById(menuName);
menu.style.left = getElementX(target) + "px";
menu.style.top = getElementY(target) + target.offsetHeight + 2 + "px";
var width = getWidth(target);
if (width > 0) {
menu.style.width = width + "px";
}
var url = "faces/ajax-autocomplete?method=" + escape(method) + "&prefix=" + escape(target.value);
initRequest(url);

if (!requests) {
requests = new Object();
}
requests.menu = menu;
requests.onchoose = onchoose;
requests.ondisplay = ondisplay;
requests.targetName = targetName;

req.onreadystatechange = processRequest;
req.open("GET", url, true);
req.send(null);
}
}

function doCompletion(ev, targetName, menuName, method, onchoose, ondisplay) {
var menu = document.getElementById(menuName);
var evt = (typeof(ev.keyCode) == 'undefined') ? window.event : ev; //IE reports window.event not arg
//alert('ev.keyCode is '+ev.keyCode+' and window.event.keyCode is '+ window.event.keyCode);
/*
Try #1: I am trying to allow people to select items with the arrow keys as
well. Not just on a click of the anchor.
*/
/****************ADDED CODE BEGIN***********/
if (typeof(evt) != 'undefined') { // in firefox, focus event may leave evt undefined.
if (evt.keyCode == 38 /*up*/ || evt.keyCode == 40 /*down*/) {
try {
var childAnchors = menu.getElementsByTagName("a");
var targetAnchor = childAnchors[0];
// Look for highlighted item. If found
for (var i = 0; i < childAnchors.length; i++) {
if (childAnchors[i].className == "selectedPopupItem") {
if (evt.keyCode == 38 && i > 0) {
childAnchors[i].className = "popupItem";
targetAnchor = childAnchors[i - 1];
}
else if (evt.keyCode == 40 && i < childAnchors.length - 1) {
childAnchors[i].className = "popupItem";
targetAnchor = childAnchors[i + 1];
}
else {
targetAnchor = childAnchors[i];
}
break;
}
}
targetAnchor.className = "selectedPopupItem";
} catch (e) { alert("error");}
return;
}
else if (evt.keyCode == 13 /*Enter*/){
try{
var childAnchors = menu.getElementsByTagName("a");
for (var i = 0; i < childAnchors.length; i++) {
if (childAnchors[i].className == "selectedPopupItem") {
try{
childAnchors[i].click(); // It appears that firefox has no "click()" defined for anchors??
} catch(e) {
childAnchors[i].onclick();
}
stopCompletion(menuName);
return;
}
}
} catch(e) {}
}
else if (evt.keyCode == 27 /* Escape */ ) {
stopCompletion(menuName);
return;
}
}
/****************more added code...***************/
// 1/1/2010, mfons, added the following call in order to avoid launching an
// ajax-request (i.e., doing a database query) for each keystroke.
// This wait make sure that the target value does not change for some
// time period (500ms at the moment) before actually doing the query.
var target = document.getElementById(targetName);
//alert(target.value);
setTimeout("doCompletionDelayed('" + targetName + "', " +
"'" + menuName + "', '" + method + "', '" + onchoose + "', " +
"'" + ondisplay + "', '" + target.value + "')", 500);
/****************ADDED CODE END***************/

}

function chooseItem(targetName, item) {
if (!requests.onchoose || requests.onchoose == "null") {
var target = document.getElementById(targetName);
target.value = item;
} else {
requests.onchoose(item);
}
}

function stopCompletion(menuName) {
var menu = document.getElementById(menuName);
if (menu != null) {
clearItems(menu);
menu.style.visibility = "hidden";
}
}

/* Stop completion shortly.
This is necessary because I want to stop completion from the blur
(focus loss event) of the completion text field, but that will also
happen, right BEFORE a link click in the completion dialog is processed.
If this is done synchronously, the link is deleted before it is processed
by stop completion. Therefore, I use the delayed variety which schedules
stop completion instead such that the link is processed first.
*/
function stopCompletionDelayed(menuName) {
/* Would like to shorten timeout but this seems to trip up Safari */
setTimeout("stopCompletion('" + menuName + "')", 400);
}

function processRequest() {
if (req.readyState == 4) {
if (req.status == 200) {
parseMessages(requests.menu);
} else if (req.status == 204){
clearItems(requests.menu);
}
}
}

function parseMessages(menu) {
clearItems(menu);
menu.style.visibility = "visible";
//alert(req.responseText);
var lItemsRE = new RegExp("<item>(.*?)</item>", "g");
var lItems = req.responseText.match(lItemsRE);
for (var i = 0; i < lItems.length; i++) {
//alert("item "+ i+ " is " + lItems[i].replace(lItemsRE, "$1"));
appendItem(menu, lItems[i].replace(lItemsRE, "$1"));
}

// var items = req.responseXML.getElementsByTagName("items")[0];
// for (loop = 0; loop < items.childNodes.length; loop++) {
//
// var item = items.childNodes[loop];
//alert('item is '+ item); // why is item null now? xml is coming back OK, it appears.
// appendItem(menu, item.childNodes[0].nodeValue);
// }
}

function clearItems(menu) {
if (menu) {
for (loop = menu.childNodes.length -1; loop >= 0 ; loop--) {
menu.removeChild(menu.childNodes[loop]);
}
}
}

function appendItem(menu, name) {
var item = document.createElement("div");
menu.appendChild(item);
var linkElement = document.createElement("a");
linkElement.className = "popupItem";
linkElement.href = "#";
linkElement.onclick = function() {
chooseItem(requests.targetName, name);
stopCompletion();
return false;
}
var displayName = name;
if (requests.ondisplay && requests.ondisplay != "null") {
displayName = requests.ondisplay(name);
}
linkElement.appendChild(document.createTextNode(displayName));
item.appendChild(linkElement);
}

This code has been tested against IE 7 and 8 and also the latest version of FF.

Please let me know if this is helpful.


No comments: