includes/clientside/static/autocomplete.js
author Dan
Fri, 12 Oct 2007 14:41:51 -0400
changeset 184 d74ff822acc9
parent 73 0a74676a2f2f
child 263 d57af0b0302e
permissions -rw-r--r--
Replaced autocompleting username with a much more efficient algorithm and caching system

/*
 * Auto-completing page/username fields
 */

// The ultimate Javascript app: AJAX auto-completion, which responds to up/down arrow keys, the enter key, and the escape key
// The idea was pilfered mercilessly from vBulletin, but uses about 8
// bytes of vB code. All the rest was coded by me, Mr. Javascript Newbie...

// ...in about 8 hours.
// You folks better like this stuff.

function nameCompleteEventHandler(e)
{
  if(!e) e = window.event;
  switch(e.keyCode)
  {
    case 38: // up
      unSelectMove('up');
      break;
    case 40: // down
      unSelectMove('down');
      break;
    case 27: // escape
    case 9:  // tab
      destroyUsernameDropdowns();
      break;
    case 13: // enter
      unSelect();
      break;
    default: return false; break;
  }
  return true;
}

function unSelectMove(dir)
{
  if(submitAuthorized) return false;
  var thediv = document.getElementById(unObjDivCurrentId);
  thetable = thediv.firstChild;
  cel = thetable.firstChild.firstChild;
  d = true;
  index = false;
  changed = false;
  // Object of the game: extract the username, determine its index in the userlist array, and then color the menu items and set unObjCurrentSelection
  while(d) // Set to false if an exception occurs or if we arrive at our destination
  {
    //*
    if(!cel) d=false;
    celbak = cel;
    cel = cel.nextSibling;
    if(!cel) d=false;
    try {
      if(cel.firstChild.nextSibling) html = cel.firstChild.nextSibling.innerHTML;
      else html = cel.firstChild.innerHTML;
      cel.firstChild.className = 'row1';
      if(cel.firstChild.nextSibling) cel.firstChild.nextSibling.className = 'row1';
      thename = html.substr(7, html.length-15);
      // FINALLY! we have extracted the username
      // Now get its position in the userlist array
      if(thename == unObjCurrentSelection)
      {
        index = parseInt(in_array(thename, userlist));
      }
      if(typeof(index) == 'number')
      {
        if(dir=='down')
          n = index+1;
        else if(dir == 'up')
          n = index - 1;
        
        // Try to trap moving the selection up or down beyond the top of bottom
        if(n > userlist.length-1 || n < 0)
        {
          cel.firstChild.className = 'row2';
          if(cel.firstChild.nextSibling) cel.firstChild.nextSibling.className = 'row2';
          return;
        }
        
        if(dir=='down') no=cel.nextSibling;
        else if(dir=='up') no=cel.previousSibling;
        no.firstChild.className = 'row2';
        if(no.firstChild.nextSibling) no.firstChild.nextSibling.className = 'row2';
        if(no.firstChild.id)
        {
          scroll = getScrollOffset() + getHeight();
          elemht = getElementHeight(no.firstChild.id);
          elemoff = fetch_offset(no.firstChild);
          whereto = elemoff['top'] + elemht;
          if(whereto > scroll)
          {
            window.location.hash = '#'+no.firstChild.id;
            unObj.focus();
          }
        }
        cel=cel.nextSibling;
        unObjCurrentSelection = userlist[n];
        index = false;
        changed = true;
        return;
      }
    } catch(e) { }
    //*/ d = false;
  }
}

function unSelect()
{
  if(!unObj || submitAuthorized) return false;
  if ( unObjCurrentSelection )
    unObj.value = unObjCurrentSelection;
  destroyUsernameDropdowns(); 
}

function in_array(needle, haystack)
{
  for(var i in haystack)
  {
    if(haystack[i] == needle) return i;
  }
  return false;
}

function ajaxUserNameComplete(o)
{
  if(!o) {destroyUsernameDropdowns(); return;}
  if(!o.value) {destroyUsernameDropdowns(); return;}
  if(o.value.length < 3) {destroyUsernameDropdowns(); return;}
  //if(IE) return; // This control doesn't work in IE. Yes, it's true! document.createElement doesn't work.
  if(!o.id)
  {
    o.id = 'usernametextboxobj_' + Math.floor(Math.random() * 10000000);
  }
  unObj = o;
  o.setAttribute("autocomplete","off");
  o.onkeyup = function(e, o) { o=unObj; if(!nameCompleteEventHandler(e)) ajaxUserNameComplete(o); }
  val = escape(o.value).replace('+', '%2B');
  ajaxGet(stdAjaxPrefix+'&_mode=fillusername&name='+val, function()
    {
      if(ajax.readyState==4)
      {
        // Determine the appropriate left/top positions, then create a div to use for the drop-down list
        // The trick here is to be able to make the div dynamically destroy itself depending on how far the user's mouse is from it
        destroyUsernameDropdowns();
        off = fetch_offset(unObj);
        dim = fetch_dimensions(unObj);
        left = off['left'];
        i1 = off['top'];
        i2 = dim['h'];
        var top = 0;
        top = i1 + i2;
        var thediv = document.createElement('div');
        thediv.className = 'tblholder';
        thediv.style.marginTop = '0px';
        thediv.style.position = 'absolute';
        thediv.style.top  = top  + 'px';
        thediv.style.left = left + 'px';
        thediv.style.zIndex = getHighestZ() + 2;
        id = 'usernamehoverobj_' + Math.floor(Math.random() * 10000000);
        unObjDivCurrentId = id;
        thediv.id = id;
        unObj.onblur = function() { destroyUsernameDropdowns(); }
        
        var response = String(ajax.responseText) + ' ';
        if ( response.substr(0,1) != '{' )
        {
          new messagebox(MB_OK|MB_ICONSTOP, 'Invalid response', 'Invalid or unexpected JSON response from server:<pre>' + ajax.responseText + '</pre>');
          return false;
        }
        
        response = parseJSON(response);
        var errorstring = false;
        if ( response.mode == 'error' )
        {
          errorstring = response.error;
        }
        else
        {
          var userlist = response.users_real;
        }
        
        if(errorstring)
        {
          html = '<span style="color: #555; padding: 4px;">'+errorstring+'</span>';
        }
        else
        {
          html = '<table border="0" cellspacing="1" cellpadding="3" style="width: auto;"><tr><th><small>Username matches</small></th></tr>';
          cls = 'row2';
          unObjCurrentSelection = userlist[0];
          for(i=0;i<userlist.length;i++)
          {
            tmpnam = 'listobjnode_'+Math.floor(Math.random() * 10000000);
            html = html + '<tr><td id="'+tmpnam+'" class="'+cls+'" style="cursor: pointer;" onclick="document.getElementById(\''+unObj.id+'\').value=\''+userlist[i]+'\';destroyUsernameDropdowns();"><small>'+userlist[i]+'</small></td></tr>';
            if(cls=='row2') cls='row1';
          }
          html = html + '</table>';
        }
        
        thediv.innerHTML = html;
        var body = document.getElementsByTagName('body');
        body = body[0];
        unSelectMenuOn = true;
        submitAuthorized = false;
        body.appendChild(thediv);
      }
    });
}

function ajaxPageNameComplete(o)
{
  if(!o) {destroyUsernameDropdowns(); return;}
  if(!o.value) {destroyUsernameDropdowns(); return;}
  if(o.value.length < 3) {destroyUsernameDropdowns(); return;}
  if(IE) return; // This control doesn't work in IE. Yes, it's true! document.createElement doesn't work.
  if(!o.id)
  {
    o.id = 'usernametextboxobj_' + Math.floor(Math.random() * 10000000);
  }
  o.setAttribute("autocomplete","off");
  unObj = o;
  o.onkeyup = function(e, o) { o=unObj; if(!nameCompleteEventHandler(e)) ajaxPageNameComplete(o); }
  val = escape(o.value).replace('+', '%2B');
  ajaxGet(stdAjaxPrefix+'&_mode=fillpagename&name='+val, function()
    {
      if(!ajax) return;
      if(ajax.readyState==4)
      {
        // Determine the appropriate left/top positions, then create a div to use for the drop-down list
        // The trick here is to be able to make the div dynamically destroy itself depending on how far the user's mouse is from it
        destroyUsernameDropdowns();
        off = fetch_offset(unObj);
        dim = fetch_dimensions(unObj);
        left = off['left'];
        top = off['top'] + dim['h'];
        var thediv = document.createElement('div');
        thediv.className = 'tblholder';
        thediv.style.marginTop = '0px';
        thediv.style.position = 'absolute';
        thediv.style.top  = top  + 'px';
        thediv.style.left = left + 'px';
        thediv.style.zIndex = getHighestZ() + 2;
        id = 'usernamehoverobj_' + Math.floor(Math.random() * 10000000);
        unObjDivCurrentId = id;
        thediv.id = id;
        
        eval(ajax.responseText);
        if(errorstring)
        {
          html = '<span style="color: #555; padding: 4px;">'+errorstring+'</span>';
        }
        else
        {
          html = '<table border="0" cellspacing="1" cellpadding="3" style="width: auto;"><tr><th colspan="2">Page name matches</th></tr><tr><th><small>Page title</small></th><th><small>Page ID</small></th></tr>';
          cls = 'row2';
          unObjCurrentSelection = userlist[0];
          for(i=0;i<userlist.length;i++)
          {
            tmpnam = 'listobjnode_'+Math.floor(Math.random() * 10000000);
            html = html + '<tr><td id="'+tmpnam+'" class="'+cls+'" style="cursor: pointer;" onclick="document.getElementById(\''+unObj.id+'\').value=\''+userlist[i]+'\';destroyUsernameDropdowns();"><small>'+namelist[i]+'</small></td><td class="'+cls+'" style="cursor: pointer;" onclick="document.getElementById(\''+unObj.id+'\').value=\''+userlist[i]+'\';destroyUsernameDropdowns();"><small>'+userlist[i]+'</small></td></tr>';
            if(cls=='row2') cls='row1';
          }
          html = html + '</table>';
        }
        
        thediv.innerHTML = html;
        var body = document.getElementsByTagName('body');
        body = body[0];
        unSelectMenuOn = true;
        submitAuthorized = false;
        body.appendChild(thediv);
      }
    });
}

function destroyUsernameDropdowns()
{
  var divs = document.getElementsByTagName('div');
  var prefix = 'usernamehoverobj_';
  for(i=0;i<divs.length;i++)                                                                                                                                                                                                                         
  {
    if ( divs[i].id )
    {
      if(divs[i].id.substr(0, prefix.length)==prefix)
      {
        divs[i].innerHTML = '';
        divs[i].style.display = 'none';
      }
    }
  }
  unSelectMenuOn = false;
  unObjDivCurrentId = false;
  unObjCurrentSelection = false;
  submitAuthorized = true;
}

function get_parent_form(o)
{
  if ( !o.parentNode )
    return false;
  if ( o.tagName == 'FORM' )
    return o;
  var p = o.parentNode;
  while(true)
  {
    if ( p.tagName == 'FORM' )
      return p;
    else if ( !p )
      return false;
    else
      p = p.parentNode;
  }
}