# HG changeset patch # User Dan # Date 1192214511 14400 # Node ID d74ff822acc9252635ba4e8cd6c0a8d8b8093f47 # Parent 91127e62f38f6ed549c9553381072c41c3b97bfb Replaced autocompleting username with a much more efficient algorithm and caching system diff -r 91127e62f38f -r d74ff822acc9 ajax.php --- 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; } diff -r 91127e62f38f -r d74ff822acc9 includes/clientside/static/acl.js --- 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"); diff -r 91127e62f38f -r d74ff822acc9 includes/clientside/static/autocomplete.js --- 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:
' + ajax.responseText + '
'); + return false; + } + + response = parseJSON(response); + var errorstring = false; + if ( response.mode == 'error' ) + { + errorstring = response.error; + } + else + { + var userlist = response.users_real; + } + if(errorstring) { html = ''+errorstring+''; diff -r 91127e62f38f -r d74ff822acc9 includes/clientside/static/autofill.js --- /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:
' + ajax.responseText + '
'); + 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... + // --> + // + // 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... + // <-- + // + // 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; +} + diff -r 91127e62f38f -r d74ff822acc9 includes/clientside/static/enano-lib-basic.js --- 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', diff -r 91127e62f38f -r d74ff822acc9 includes/functions.php --- 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>/is', $html, $jscript); + preg_match_all('/(.*?)(\]\]>)?<\/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', diff -r 91127e62f38f -r d74ff822acc9 includes/template.php --- 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 = '