Replaced autocompleting username with a much more efficient algorithm and caching system
--- a/ajax.php Tue Oct 09 16:14:55 2007 -0400
+++ b/ajax.php Fri Oct 12 14:41:51 2007 -0400
@@ -33,35 +33,50 @@
define('ENANO_ROOT', dirname($filename));
require(ENANO_ROOT.'/includes/functions.php');
require(ENANO_ROOT.'/includes/dbal.php');
+ require(ENANO_ROOT.'/includes/json.php');
$db = new mysql();
$db->connect();
- // should be connected now
+ // result is sent using JSON
+ $json = new Services_JSON(SERVICES_JSON_LOOSE_TYPE);
+ $return = Array(
+ 'mode' => 'success',
+ 'users_real' => Array()
+ );
+
+ // should be connected to the DB now
$name = (isset($_GET['name'])) ? $db->escape($_GET['name']) : false;
if ( !$name )
{
- die('userlist = new Array(); errorstring=\'Invalid URI\'');
+ $return = array(
+ 'mode' => 'error',
+ 'error' => 'Invalid URI'
+ );
+ die( $json->encode($return) );
}
- $q = $db->sql_query('SELECT username,user_id FROM '.table_prefix.'users WHERE lcase(username) LIKE lcase(\'%'.$name.'%\');');
+ $allowanon = ( isset($_GET['allowanon']) && $_GET['allowanon'] == '1' ) ? '' : ' AND user_id > 1';
+ $q = $db->sql_query('SELECT username FROM '.table_prefix.'users WHERE lcase(username) LIKE lcase(\'%'.$name.'%\')' . $allowanon . ' ORDER BY username ASC;');
if ( !$q )
{
- die('userlist = new Array(); errorstring=\'MySQL error selecting username data: '.addslashes(mysql_error()).'\'');
+ $return = array(
+ 'mode' => 'error',
+ 'error' => 'MySQL error selecting username data: '.addslashes(mysql_error())
+ );
+ die( $json->encode($return) );
}
- if($db->numrows() < 1)
- {
- die('userlist = new Array(); errorstring=\'No usernames found\';');
- }
- echo 'var errorstring = false; userlist = new Array();';
$i = 0;
while($r = $db->fetchrow())
{
- echo "userlist[$i] = '".addslashes($r['username'])."'; ";
+ $return['users_real'][] = $r['username'];
$i++;
}
$db->free_result();
// all done! :-)
$db->close();
+
+ echo $json->encode( $return );
+
exit;
}
--- a/includes/clientside/static/acl.js Tue Oct 09 16:14:55 2007 -0400
+++ b/includes/clientside/static/acl.js Fri Oct 12 14:41:51 2007 -0400
@@ -128,7 +128,7 @@
usrsel = document.createElement('input');
usrsel.type = 'text';
usrsel.name = 'username';
- usrsel.onkeyup = function() { ajaxUserNameComplete(this); };
+ usrsel.onkeyup = function() { new AutofillUsername(this, undefined, true); };
usrsel.id = 'userfield_' + aclManagerID;
try {
usrsel.setAttribute("autocomplete","off");
--- a/includes/clientside/static/autocomplete.js Tue Oct 09 16:14:55 2007 -0400
+++ b/includes/clientside/static/autocomplete.js Fri Oct 12 14:41:51 2007 -0400
@@ -160,7 +160,24 @@
thediv.id = id;
unObj.onblur = function() { destroyUsernameDropdowns(); }
- eval(ajax.responseText);
+ 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>';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/includes/clientside/static/autofill.js Fri Oct 12 14:41:51 2007 -0400
@@ -0,0 +1,512 @@
+/**
+ * Javascript auto-completion for form fields.
+ */
+
+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($(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 = $(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('Username 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('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 = $(autofill.field_id).Top();
+ var tb_height = $(autofill.field_id).Height();
+ var af_top = tb_top + tb_height - 9;
+ var tb_left = $(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 )
+ {
+ 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 ( $(afobj.field_id).object.value.length < 3 )
+ return false;
+ var resp_json = parseJSON(response);
+ var resp_code = $(afobj.field_id).object.value.toLowerCase().substr(0, 3);
+ afobj.responses[resp_code] = resp_json;
+ afobj.process_dataset(resp_json);
+ }
+ }
+ var usernamefragment = ajaxEscape($(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 = $(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;
+ }
+ }
+ form._af_acting = true;
+ }
+ }
+
+ this.keyhandler = function()
+ {
+ var key = this.event.keyCode;
+ if ( key == this.KEY_ENTER && !this.repeat )
+ {
+ var form = findParentForm($(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($(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 = $(this.box_id).Height();
+ var top = $(this.box_id).object.scrollTop;
+ var scroll_bottom = height + top;
+
+ var td_top = $(state_td.parentNode.nextSibling.firstChild).Top() - $(this.box_id).Top();
+ var td_height = $(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);
+ $(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
+ $(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 = $(this.box_id).object.scrollTop;
+
+ var td_top = $(state_td.parentNode.previousSibling.firstChild).Top() - $(this.box_id).Top();
+
+ if ( td_top < top )
+ {
+ $(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
+ $(this.field_id).object.focus();
+ }
+ }
+ else
+ {
+ $(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.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();
+ }
+
+ this.sleep = function()
+ {
+ if ( this.box_id )
+ {
+ var div = document.getElementById(this.box_id);
+ div.style.display = 'none';
+ }
+ var el = $(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;
+}
+
--- a/includes/clientside/static/enano-lib-basic.js Tue Oct 09 16:14:55 2007 -0400
+++ b/includes/clientside/static/enano-lib-basic.js Fri Oct 12 14:41:51 2007 -0400
@@ -264,6 +264,7 @@
'admin-menu.js',
'ajax.js',
'autocomplete.js',
+ 'autofill.js',
'base64.js',
'dropdown.js',
'faders.js',
--- a/includes/functions.php Tue Oct 09 16:14:55 2007 -0400
+++ b/includes/functions.php Fri Oct 12 14:41:51 2007 -0400
@@ -2804,7 +2804,7 @@
}
// Optimize (but don't obfuscate) Javascript
- preg_match_all('/<script([ ]+.*?)?>(.*?)<\/script>/is', $html, $jscript);
+ preg_match_all('/<script([ ]+.*?)?>(.*?)(\]\]>)?<\/script>/is', $html, $jscript);
// list of Javascript reserved words - from about.com
$reserved_words = array('abstract', 'as', 'boolean', 'break', 'byte', 'case', 'catch', 'char', 'class', 'continue', 'const', 'debugger', 'default', 'delete', 'do',
--- a/includes/template.php Tue Oct 09 16:14:55 2007 -0400
+++ b/includes/template.php Fri Oct 12 14:41:51 2007 -0400
@@ -1411,7 +1411,7 @@
function username_field($name, $value = false)
{
$randomid = md5( time() . microtime() . mt_rand() );
- $text = '<input name="'.$name.'" onkeyup="ajaxUserNameComplete(this)" autocomplete="off" type="text" size="30" id="userfield_'.$randomid.'"';
+ $text = '<input name="'.$name.'" onkeyup="new AutofillUsername(this);" autocomplete="off" type="text" size="30" id="userfield_'.$randomid.'"';
if($value) $text .= ' value="'.$value.'"';
$text .= ' />';
return $text;