includes/clientside/static/autocomplete.js
author Dan
Fri, 05 Oct 2007 01:57:00 -0400
changeset 162 e1a22031b5bd
parent 73 0a74676a2f2f
child 184 d74ff822acc9
permissions -rw-r--r--
Major revamps to the template parser. Fixed a few security holes that could allow PHP to be injected in untimely places in TPL code. Improved Ux for XSS attempt in tplWikiFormat. Documented many functions. Backported much cleaner parser from 2.0 branch. Beautified a lot of code in the depths of the template class. Pretty much a small-scale Extreme Makeover.

/*
 * 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(); }
        
        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><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;
  }
}