includes/clientside/static/autofill.js
author Dan
Wed, 26 Mar 2008 02:56:23 -0400
changeset 509 175df10e0b56
parent 420 301f546688d1
child 550 685e839d934e
permissions -rw-r--r--
Added a copy of Firebug Lite for debugging purposes. License is uncertain but being treated as MPL. (If is is not MPL then it is under something more permissive that permits relicensing anyway)

/**
 * Javascript auto-completion for form fields. This supercedes the code in autocomplete.js for MOZILLA ONLY. It doesn't seem to work real
 * well with other browsers yet.
 */
 
var af_current = false;
 
function AutofillUsername(parent, event, allowanon)
{
  // if this is IE, use the old code
  if ( IE )
  {
    ajaxUserNameComplete(parent);
    return false;
  }
  if ( parent.afobj )
  {
    parent.afobj.go();
    return true;
  }
  
  parent.autocomplete = 'off';
  parent.setAttribute('autocomplete', 'off');
  
  this.repeat = false;
  this.event = event;
  this.box_id = false;
  this.boxes = new Array();
  this.state = false;
  this.allowanon = ( allowanon ) ? true : false;
  
  if ( !parent.id )
    parent.id = 'afuser_' + Math.floor(Math.random() * 1000000);
  
  this.field_id = parent.id;
  
  // constants
  this.KEY_UP    = 38;
  this.KEY_DOWN  = 40;
  this.KEY_ESC   = 27;
  this.KEY_TAB   = 9;
  this.KEY_ENTER = 13;
  
  // response cache
  this.responses = new Object();
  
  // ajax placeholder
  this.process_dataset = function(resp_json)
  {
    // window.console.info('Processing the following dataset.');
    // window.console.debug(resp_json);
    var autofill = this;
    
    if ( typeof(autofill.event) == 'object' )
    {
      if ( autofill.event.keyCode )
      {
        if ( autofill.event.keyCode == autofill.KEY_ENTER && autofill.boxes.length < 1 && !autofill.box_id )
        {
          // user hit enter after accepting a suggestion - submit the form
          var frm = findParentForm($dynano(autofill.field_id).object);
          frm._af_acting = false;
          frm.submit();
          // window.console.info('Submitting form');
          return false;
        }
        if ( autofill.event.keyCode == autofill.KEY_UP || autofill.event.keyCode == autofill.KEY_DOWN || autofill.event.keyCode == autofill.KEY_ESC || autofill.event.keyCode == autofill.KEY_TAB || autofill.event.keyCode == autofill.KEY_ENTER )
        {
          autofill.keyhandler();
          // window.console.info('Control key detected, called keyhandler and exiting');
          return true;
        }
      }
    }
    
    if ( this.box_id )
    {
      this.destroy();
      // window.console.info('already have a box open - destroying and exiting');
      //return false;
    }
    
    var users = new Array();
    for ( var i = 0; i < resp_json.users_real.length; i++ )
    {
      try
      {
        var user = resp_json.users_real[i].toLowerCase();
        var inp  = $dynano(autofill.field_id).object.value;
        inp = inp.toLowerCase();
        if ( user.indexOf(inp) > -1 )
        {
          users.push(resp_json.users_real[i]);
        }
      }
      catch(e)
      {
        users.push(resp_json.users_real[i]);
      }
    }

    // This was used ONLY for debugging the DOM and list logic    
    // resp_json.users = resp_json.users_real;
    
    // construct table
    var div = document.createElement('div');
    div.className = 'tblholder';
    div.style.clip = 'rect(0px,auto,auto,0px)';
    div.style.maxHeight = '200px';
    div.style.overflow = 'auto';
    div.style.zIndex = '9999';
    var table = document.createElement('table');
    table.border = '0';
    table.cellSpacing = '1';
    table.cellPadding = '3';
    
    var tr = document.createElement('tr');
    var th = document.createElement('th');
    th.appendChild(document.createTextNode($lang.get('user_autofill_heading_suggestions')));
    tr.appendChild(th);
    table.appendChild(tr);
    
    if ( users.length < 1 )
    {
      var tr = document.createElement('tr');
      var td = document.createElement('td');
      td.className = 'row1';
      td.appendChild(document.createTextNode($lang.get('user_autofill_msg_no_suggestions')));
      td.afobj = autofill;
      tr.appendChild(td);
      table.appendChild(tr);
    }
    else
      
      for ( var i = 0; i < users.length; i++ )
      {
        var user = users[i];
        var tr = document.createElement('tr');
        var td = document.createElement('td');
        td.className = ( i == 0 ) ? 'row2' : 'row1';
        td.appendChild(document.createTextNode(user));
        td.afobj = autofill;
        td.style.cursor = 'pointer';
        td.onclick = function()
        {
          this.afobj.set(this.firstChild.nodeValue);
        }
        tr.appendChild(td);
        table.appendChild(tr);
      }
      
    // Finalize div
    var tb_top    = $dynano(autofill.field_id).Top();
    var tb_height = $dynano(autofill.field_id).Height();
    var af_top    = tb_top + tb_height - 9;
    var tb_left   = $dynano(autofill.field_id).Left();
    var af_left   = tb_left;
    
    div.style.position = 'absolute';
    div.style.left = af_left + 'px';
    div.style.top  = af_top  + 'px';
    div.style.width = '200px';
    div.style.fontSize = '7pt';
    div.style.fontFamily = 'Trebuchet MS, arial, helvetica, sans-serif';
    div.id = 'afuserdrop_' + Math.floor(Math.random() * 1000000);
    div.appendChild(table);
    
    autofill.boxes.push(div.id);
    autofill.box_id = div.id;
    if ( users.length > 0 )
      autofill.state = users[0];
    
    var body = document.getElementsByTagName('body')[0];
    body.appendChild(div);
    
    autofill.repeat = true;
  }
  
  // perform ajax call
  this.fetch_and_process = function()
  {
    af_current = this;
    var processResponse = function()
    {
      if ( ajax.readyState == 4 && ajax.status == 200 )
      {
        var afobj = af_current;
        af_current = false;
        // parse the JSON response
        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;
        }
        if ( $dynano(afobj.field_id).object.value.length < 3 )
          return false;
        var resp_json = parseJSON(response);
        var resp_code = $dynano(afobj.field_id).object.value.toLowerCase().substr(0, 3);
        afobj.responses[resp_code] = resp_json;
        afobj.process_dataset(resp_json);
      }
    }
    var usernamefragment = ajaxEscape($dynano(this.field_id).object.value);
    ajaxGet(stdAjaxPrefix + '&_mode=fillusername&name=' + usernamefragment + '&allowanon=' + ( this.allowanon ? '1' : '0' ), processResponse);
  }
  
  this.go = function()
  {
    if ( document.getElementById(this.field_id).value.length < 3 )
    {
      this.destroy();
      return false;
    }
    
    if ( af_current )
      return false;
    
    var resp_code = $dynano(this.field_id).object.value.toLowerCase().substr(0, 3);
    if ( this.responses.length < 1 || ! this.responses[ resp_code ] )
    {
      // window.console.info('Cannot find dataset ' + resp_code + ' in cache, sending AJAX request');
      this.fetch_and_process();
    }
    else
    {
      // window.console.info('Using cached dataset: ' + resp_code);
      var resp_json = this.responses[ resp_code ];
      this.process_dataset(resp_json);
    }
    document.getElementById(this.field_id).onkeyup = function(event)
    {
      this.afobj.event = event;
      this.afobj.go();
    }
    document.getElementById(this.field_id).onkeydown = function(event)
    {
      var form = findParentForm(this);
      if ( typeof(event) != 'object' )
        var event = window.event;
      if ( typeof(event) == 'object' )
      {
        if ( event.keyCode == this.afobj.KEY_ENTER && this.afobj.boxes.length < 1 && !this.afobj.box_id )
        {
          // user hit enter after accepting a suggestion - submit the form
          form._af_acting = false;
          return true;
        }
        else
        {
          form._af_acting = true;
          return true;
        }
      }
    }
  }
  
  this.keyhandler = function()
  {
    var key = this.event.keyCode;
    if ( key == this.KEY_ENTER && !this.repeat )
    {
      submitAuthorized = true;
      var form = findParentForm($dynano(this.field_id).object);
      form._af_acting = false;
      return true;
    }
    switch(key)
    {
      case this.KEY_UP:
        this.focus_up();
        break;
      case this.KEY_DOWN:
        this.focus_down();
        break;
      case this.KEY_ESC:
        this.destroy();
        break;
      case this.KEY_TAB:
        this.destroy();
        break;
      case this.KEY_ENTER:
        this.set();
        break;
    }
    
    var form = findParentForm($dynano(this.field_id).object);
      form._af_acting = false;
  }
  
  this.get_state_td = function()
  {
    var div = document.getElementById(this.box_id);
    if ( !div )
      return false;
    if ( !this.state )
      return false;
    var table = div.firstChild;
    for ( var i = 1; i < table.childNodes.length; i++ )
    {
      // the table is DOM-constructed so no cruddy HTML hacks :-)
      var child = table.childNodes[i];
      var tn = child.firstChild.firstChild;
      if ( tn.nodeValue == this.state )
        return child.firstChild;
    }
    return false;
  }
  
  this.focus_down = function()
  {
    var state_td = this.get_state_td();
    if ( !state_td )
      return false;
    if ( state_td.parentNode.nextSibling )
    {
      // Ooh boy, DOM stuff can be so complicated...
      //   <tr>   →   <tr>
      // ↑ <td>       <td> ↓
      //   user       user
      
      var newstate = state_td.parentNode.nextSibling.firstChild.firstChild.nodeValue;
      if ( !newstate )
        return false;
      this.state = newstate;
      state_td.className = 'row1';
      state_td.parentNode.nextSibling.firstChild.className = 'row2';
      
      // Exception - automatically scroll around if the item is off-screen
      var height = $dynano(this.box_id).Height();
      var top = $dynano(this.box_id).object.scrollTop;
      var scroll_bottom = height + top;
      
      var td_top = $dynano(state_td.parentNode.nextSibling.firstChild).Top() - $dynano(this.box_id).Top();
      var td_height = $dynano(state_td.parentNode.nextSibling.firstChild).Height();
      var td_bottom = td_top + td_height;
      
      if ( td_bottom > scroll_bottom )
      {
        var scrollY = td_top - height + 2*td_height - 7;
        // window.console.debug(scrollY);
        $dynano(this.box_id).object.scrollTop = scrollY;
        /*
        var newtd = state_td.parentNode.nextSibling.firstChild;
        var a = document.createElement('a');
        var id = 'autofill' + Math.floor(Math.random() * 100000);
        a.name = id;
        a.id = id;
        newtd.appendChild(a);
        window.location.hash = '#' + id;
        */
        
        // In firefox, scrolling like that makes the field get unfocused
        $dynano(this.field_id).object.focus();
      }
    }
    else
    {
      return false;
    }
  }
  
  this.focus_up = function()
  {
    var state_td = this.get_state_td();
    if ( !state_td )
      return false;
    if ( state_td.parentNode.previousSibling && state_td.parentNode.previousSibling.firstChild.tagName != 'TH' )
    {
      // Ooh boy, DOM stuff can be so complicated...
      //   <tr>   ←   <tr>
      // ↓ <td>       <td> ↑
      //   user       user
      
      var newstate = state_td.parentNode.previousSibling.firstChild.firstChild.nodeValue;
      if ( !newstate )
      {
        return false;
      }
      this.state = newstate;
      state_td.className = 'row1';
      state_td.parentNode.previousSibling.firstChild.className = 'row2';
      
      // Exception - automatically scroll around if the item is off-screen
      var top = $dynano(this.box_id).object.scrollTop;
      
      var td_top = $dynano(state_td.parentNode.previousSibling.firstChild).Top() - $dynano(this.box_id).Top();
      
      if ( td_top < top )
      {
        $dynano(this.box_id).object.scrollTop = td_top - 10;
        /*
        var newtd = state_td.parentNode.previousSibling.firstChild;
        var a = document.createElement('a');
        var id = 'autofill' + Math.floor(Math.random() * 100000);
        a.name = id;
        a.id = id;
        newtd.appendChild(a);
        window.location.hash = '#' + id;
        */
        
        // In firefox, scrolling like that makes the field get unfocused
        $dynano(this.field_id).object.focus();
      }
    }
    else
    {
      $dynano(this.box_id).object.scrollTop = 0;
      return false;
    }
  }
  
  this.destroy = function()
  {
    this.repeat = false;
    var body = document.getElementsByTagName('body')[0];
    var div = document.getElementById(this.box_id);
    if ( !div )
      return false;
    setTimeout('var body = document.getElementsByTagName("body")[0]; body.removeChild(document.getElementById("'+div.id+'"));', 20);
    // hackish workaround for divs that stick around past their welcoming period
    for ( var i = 0; i < this.boxes.length; i++ )
    {
      var div = document.getElementById(this.boxes[i]);
      if ( div )
        setTimeout('var body = document.getElementsByTagName("body")[0]; var div = document.getElementById("'+div.id+'"); if ( div ) body.removeChild(div);', 20);
      delete(this.boxes[i]);
    }
    this.boxes = new Array();
    this.box_id = false;
    this.state = false;
  }
  
  this.set = function(val)
  {
    var ta = document.getElementById(this.field_id);
    if ( val )
      ta.value = val;
    else if ( this.state )
      ta.value = this.state;
    this.destroy();
    findParentForm($dynano(this.field_id.object))._af_acting = false;
  }
  
  this.sleep = function()
  {
    if ( this.box_id )
    {
      var div = document.getElementById(this.box_id);
      div.style.display = 'none';
    }
    var el = $dynano(this.field_id).object;
    var fr = findParentForm(el);
    el._af_acting = false;
  }
  
  this.wake = function()
  {
    if ( this.box_id )
    {
      var div = document.getElementById(this.box_id);
      div.style.display = 'block';
    }
  }
  
  parent.onblur = function()
  {
    af_current = this.afobj;
    window.setTimeout('if ( af_current ) af_current.sleep(); af_current = false;', 50);
  }
  
  parent.onfocus = function()
  {
    af_current = this.afobj;
    window.setTimeout('if ( af_current ) af_current.wake(); af_current = false;', 50);
  }
  
  parent.afobj = this;
  var frm = findParentForm(parent);
  if ( frm.onsubmit )
  {
    frm.orig_onsubmit = frm.onsubmit;
    frm.onsubmit = function(e)
    {
      if ( this._af_acting )
        return false;
      this.orig_onsubmit(e);
    }
  }
  else
  {
    frm.onsubmit = function()
    {
      if ( this._af_acting )
        return false;
    }
  }
  
  if ( parent.value.length < 3 )
  {
    this.destroy();
    return false;
  }
}

function findParentForm(o)
{
  if ( o.tagName == 'FORM' )
    return o;
  while(true)
  {
    o = o.parentNode;
    if ( !o )
      return false;
    if ( o.tagName == 'FORM' )
      return o;
  }
  return false;
}