JavaScript Auto-Suggest from XML
The other day, a co-worker requested an auto-suggest feature in the user name field of a web app that I had written a few months ago. Why? Not sure - to save 6 keystrokes, I suppose - but then again, 6 keystrokes at a time, it becomes justifiable eventually... And really, who can turn down a challenge like that?
- Post-Implementation Numbers:
- Average Keystrokes saved per day: 179.4
- Characters in Scripts (Client and Server, no formatting): 4886
- Payback time: 27.24 Days
Not bad in terms of effort. Even calculating in the difference between developer salary and data entry salary - the payback is still within 3 months. (Assuming we develop in a vacuum and ignore opportunity costs - i.e. we had nothing better to do at the time.)
For starters, we need to be able to do XPath queries in FireFox as well as IE - so here we make the selectNodes method available in FireFox:
if (!window.ActiveXObject) { Element.prototype.selectNodes = function(sXPath) { var oEvaluator = new XPathEvaluator(); var oResult = oEvaluator.evaluate(sXPath, this, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null); var aNodes = new Array(); if (oResult != null) { var oElement = oResult.iterateNext(); while(oElement) { aNodes.push(oElement); oElement = oResult.iterateNext(); } } return aNodes; } }
Now we initalize everything
var autoSuggestXML; var keysDown = 0; var fragment = ''; function initAutoSuggest() { document.getElementById("updateText").focus(); if (window.ActiveXObject) { autoSuggestXML = new ActiveXObject("Msxml2.DOMDocument"); autoSuggestXML.setProperty("SelectionLanguage", "XPath"); } else { autoSuggestXML = document.implementation.createDocument("","",null); } autoSuggestXML.load("autoSuggest.xml"); registerAutoSuggestField("userInput"); }
function registerAutoSuggestField(field) { document.getElementById(field).onkeyup = function (e) { if(!e) e = window.event; autoSuggest(this,e); } document.getElementById(field).onkeydown = function (e) { keysDown++; } document.getElementById(field).onfocus = function (e) { keysDown = 0; } }
Here's some functions I borrowed from a tutorial on an AutoSuggest Javascript class out on the web for cross-browser compatable text selection
function selectRange(oTextbox /*:object*/, iStart /*:int*/, iLength /*:int*/) { //use text ranges for Internet Explorer if (oTextbox.createTextRange) { var oRange = oTextbox.createTextRange(); oRange.moveStart("character", iStart); oRange.moveEnd("character", iLength - oTextbox.value.length); oRange.select(); //use setSelectionRange() for Mozilla } else if (oTextbox.setSelectionRange) { oTextbox.setSelectionRange(iStart, iLength); } //set focus back to the textbox oTextbox.focus(); }; function typeAhead (oTextbox /*:object*/, sSuggestion /*:String*/, sFragment /*:String*/) { var iLen = oTextbox.value.length; oTextbox.value = sSuggestion; selectRange(oTextbox, iLen, sSuggestion.length); };
And now for the workhorse function
function autoSuggest(t, e) { if (window.event) keycode = window.event.keyCode; else if (e) keycode = e.which; if(--keysDown > 0 && keysDown < 3) { return; } //set to 0, just incase we passed a value greater than 2 //(happens when a key is held down) keysDown = 0; if(t.value.length > 0) { var keycode; //make sure not to interfere with non-character keys if (keycode == 38 || keycode == 40 || keycode == 33 || (keycode > 46 && keycode < 112) || keycode > 123) { var direction = ''; var previousValue = ''; var id; var value; switch (keycode) { case 38: //up direction = 'up'; previousValue = t.value.toLowerCase(); t.value = fragment; break; case 40: //down direction = 'down'; previousValue = t.value.toLowerCase(); t.value = fragment; break; default: fragment = t.value; } value = t.value.toLowerCase(); id = t.id.toLowerCase(); //if(previousValue != '') alert(previousValue + " " + value); var nodelist = null; nodelist = autoSuggestXML.documentElement.selectNodes( "field[@id='" + id + "']/value[starts-with(.,'" + fragment + "')]"); if (nodelist != null && nodelist.length > 0) { if (nodelist.length > 1 && direction != '') { var i; for(i = 0; i < nodelist.length; i++) { if(nodelist[i].firstChild.nodeValue == previousValue) { break; } } if(direction == 'up') { if (i<nodelist.length ) sSuggestion="String(nodelist[i+1].firstChild.nodeValue);" else sSuggestion = String(nodelist[0].firstChild.nodeValue); } else { if (i > 0) sSuggestion = String(nodelist[i-1].firstChild.nodeValue); else sSuggestion = String(nodelist[nodelist.length - 1].firstChild.nodeValue); } } else { var sSuggestion = String(nodelist[0].firstChild.nodeValue); } typeAhead(t, sSuggestion) } } else { //ignore } } }
Ah, now lets examine the XML schema that this thing understands (autoSuggest.xml):
<suggestions> <field id="userinput"></field> <value>Tim Laqua</value> <value>admin</value> </suggestions>
To be continued... (I'll figure out where I borrowed those functions from as well and credit the guy).



