tim laqua dot com Thoughts and Code from Tim Laqua

28Oct/070

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).

Comments (0) Trackbacks (0)

No comments yet.


Leave a comment

No trackbacks yet.