includes/clientside/static/autofill.js
author Dan
Sat, 07 Jun 2008 22:50:31 -0400
changeset 566 06d241de3151
parent 550 685e839d934e
child 581 5e8fd89c02ea
permissions -rw-r--r--
Made ajaxReset() call the actual requested title instead of effective title; fixed (again) template compiler bug not matching certain tags (probably PCRE bug)

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