/* vim:ts=4:sts=4:sw=2:noai:noexpandtab * * Auto-complete client side javascript. * Copyright (c) 2005 Steven McCoy * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ /* format of constructor is overloaded: * AC(, , ) * AC(, ) * AC() */ function AC(id) { /* find search type */ if (arguments.length > 1) { this.type = arguments[0]; id = arguments[1]; } else { this.type = id; } /* input element we are autocompleting on */ this.obj = document.getElementById(id); this.obj.value = ''; /* base url to send request too */ this.url = '/ac.php'; /* function to call when option selected */ this.submit_callback = (arguments.length > 2) ? arguments[2] : null; /* popup layer we will display results in */ this.div = document.createElement('DIV'); this.div.className = 'ac_menu'; this.div.style.visibility = 'hidden'; this.div.style.position = 'absolute'; this.div.style.zIndex = 1; this.div.style.width = this.obj.offsetWidth - 2 + "px"; this.div.style.left = this.total_offset(this.obj,'offsetLeft') + "px"; this.div.style.top = this.total_offset(this.obj,'offsetTop') + this.obj.offsetHeight - 1 + "px"; /* tie to input element */ this.obj.parentNode.insertBefore(this.div, this.obj.nextSibling); /* iframe for non-XmlHttpRequest() browsers */ this.iframe = null; /* install event handlers */ this.obj.onkeydown = this.onkeydown; this.obj.onkeyup = this.onkeyup; this.obj.onkeypress = this.onkeypress; this.obj.onblur = function() { this.AC.close_popup(); } this.obj.AC = this; /* self reference */ this.selected_option = null; /* the currently selected option */ this.request = null; /* http request object */ this.cache = new Array(); /* cache of results from server */ this.typing = false; /* whether user is still typing */ this.typing_timeout = 10; this.sending_timeout = 10; this.search_term = null; /* current search term */ this.previous_term = null; /* previous search term */ this.searched_term = null; /* search from keyboard */ this.last_input = null; /* previous typed entry */ /* Unicode inputs require polling of the input control for updates */ this.poll_input = false; /* update extern mapping array for rpc reply */ _ac_map_add(this); } AC.prototype.enable_unicode = function() { this.poll_input = true; _ac_key_check(this,this.typing_timeout); } AC.prototype.total_offset = function(element, property) { var total = 0; while (element) { total += element[property]; element = element.offsetParent; } return total; } /* hide popup and cleanup */ AC.prototype.close_popup = function() { this.div.style.visibility = 'hidden'; /* no selected item, no typing, and close any pending request */ this.selected_option = null; this.typing = false; this.search_term = null; this.previous_term = null; } /* create object for rpc call */ AC.prototype.XMLHttpRequest = function() { var request = null; if (typeof XMLHttpRequest != 'undefined') { request = new XMLHttpRequest(); } else { try { request = new ActiveXObject('Msxml2.XMLHTTP') } catch(e) { try { request = new ActiveXObject('Microsoft.XMLHTTP') } catch(e) { request = null } } } return request; } /* helper functions to process typing timer */ var _ac_key_thunk = new Array(); function _ac_key_thunk_call(id) { if (_ac_key_thunk[id]) { var ac = _ac_key_thunk[id][1]; /* now check as if onkeyup() was called */ /* first find unselected text */ var unselected = ac.obj.value; if (document.selection) { var range = document.selection.createRange(); if (range) { /* to limit the execution this would be nice, but parentElement() not supported in Opera */ // if (range && range.parentElement && range.parentElement() == ac.obj) { var length = unselected.length - range.text.length; if (length > 0) { unselected = unselected.substring(0, length); } } } else if (ac.obj.setSelectionRange) { var length = ac.obj.selectionEnd - ac.obj.selectionStart; if (length > 0) unselected = unselected.substring(0,ac.obj.selectionStart); } if (unselected != ac.last_input) { if (unselected.length > 0) { ac.searched_term = unselected; ac.suggest(ac.searched_term); } else { _ac_cancel(ac); ac.close_popup(); } ac.last_input = unselected; } /* re-install timer for polling */ if (ac.poll_input) { _ac_key_thunk[id][2] = setTimeout("_ac_key_thunk_call("+id+")",ac.typing_timeout); } else { /* remove from list and cleanup list */ _ac_key_thunk[id] = null; for (i = _ac_key_thunk.length; i > 0; i--) if (_ac_key_thunk[i] == null) _ac_key_thunk.length--; } } } function _ac_key_check(ac,timeout) { /* first remove any pending key check */ for (i = _ac_key_thunk.length-1; i >= 0; i--) if (_ac_key_thunk[i] != null && _ac_key_thunk[i][0] == ac.obj.id) { clearTimeout(_ac_key_thunk[i][2]); _ac_key_thunk[i] = null; } /* now setup a new one */ var i = _ac_key_thunk.length; var handle = setTimeout("_ac_key_thunk_call("+i+")",timeout); _ac_key_thunk[i] = new Array(ac.obj.id,ac,handle); } /* helper functions to process sending timer */ var _ac_thunk = new Array(); function _ac_thunk_call(id) { if (_ac_thunk[id]) { var ac = _ac_thunk[id][1]; ac.typing = false; ac.send(_ac_thunk[id][2]); _ac_thunk[id] = null; for (i = _ac_thunk.length; i > 0; i--) if (_ac_thunk[i] == null) _ac_thunk.length--; } } /* cancel a pending request */ function _ac_cancel(ac) { for (i = _ac_thunk.length-1; i >= 0; i--) if (_ac_thunk[i] != null && _ac_thunk[i][0] == ac.obj.id) { clearTimeout(_ac_thunk[i][3]); _ac_thunk[i] = null; } } function _ac_add(ac,query,timeout) { var i = _ac_thunk.length; var handle = setTimeout("_ac_thunk_call("+i+")",timeout); _ac_thunk[i] = new Array(ac.obj.id,ac,query,handle); } /* helper functions for webserver rpc processing */ var _ac_map = new Array(); function _ac_map_add(ac) { _ac_map[ac.obj.id] = ac; } /* called to initiation suggestion process */ AC.prototype.suggest = function(query) { /* remove redundant searches */ if (query == this.search_term) return; /* cancel any existing http call */ _ac_cancel(this); if (this.request && this.request.readyState != 0) { this.request.abort(); } /* check cache */ var lc = query.toLowerCase(); for (i = 0; i < this.cache.length; i++) if (this.cache[i][0] == lc) { var results = this.cache[i][1]; this.search_term = query; this.update_popup(results); return; } /* send call to server */ this.typing = true; this.send(query); } /* called to send message to a server */ AC.prototype.send = function(query) { /* check throttle timer */ if (this.typing) { _ac_add(this,query,this.sending_timeout); return; } /* initiate new call */ this.search_term = query; if (this.iframe == null) { this.request = this.XMLHttpRequest(); if (this.request == null) { var iframe = document.createElement('IFRAME'); iframe.src = this.url+'?i=1&id='+encodeURI(this.obj.id)+'&t='+encodeURI(this.type)+'&s='+encodeURI(query); /* opera 7.54 doesn't like iframe styles */ iframe.style.width = '0px'; iframe.style.height = '0px'; this.iframe = this.obj.appendChild(iframe); this.obj.focus(); } else { /* send XmlHttpRequest */ var AC = this; this.request.onreadystatechange = function() { if (AC.request.readyState == 4) { try { if (AC.request.status != 200 || AC.request.responseText.charAt(0) == '<') { /* some error */ } else { eval(AC.request.responseText); } } catch (e) {} } } this.request.open("GET", this.url+"?id="+encodeURI(this.obj.id)+"&t="+encodeURI(this.type)+"&s="+encodeURI(query)); this.request.send(null); } } else { /* re-submit iframe */ this.iframe.src = this.url+'?i=1&id='+encodeURI(this.obj.id)+'&t='+encodeURI(this.type)+'&s='+encodeURI(query); this.obj.focus(); } } /* called with array of search results */ AC.prototype.update_popup = function(results) { if (this.search_term != null && results != null && results.length > 0) { /* remove currently listed options */ while (this.div.hasChildNodes()) this.div.removeChild(this.div.firstChild); /* default to first result when adding characters */ if (this.previous_term == null || this.search_term.length >= this.previous_term.length) { this.selected_option = 0; } else { /* remove selection when deleteing */ this.selected_option = null; } this.previous_term = this.search_term; for (i = 0; i < results.length; i++) { var div = document.createElement('DIV'); div.divid = results[i][2]; div.AC = this; if (this.selected_option == div.divid) div.className = 'ac_highlight'; else div.className = 'ac_normal'; div.name = results[i][0]; div.value = results[i][1]; div.innerHTML = results[i][3]; div.onmousedown = function() { this.AC.onselected(); } div.onmouseover = function() { if (this.AC.selected_option != null) this.AC.div.childNodes[this.AC.selected_option].className = 'ac_normal'; this.AC.selected_option = this.divid; this.AC.cabbage = this.AC.selected_option; this.className = 'ac_highlight'; } div.onmouseout = function() { this.className = 'ac_normal'; } this.div.appendChild(div); } this.div.style.visibility = 'visible'; /* complete text box with selected text */ if (this.selected_option == 0 && (this.obj.createTextRange || this.obj.setSelectionRange) && this.obj.value != results[0][1] && results[0][1].substring(0,this.search_term.length).toLowerCase() == this.search_term.toLowerCase()) { this.obj.value = results[0][1]; if (this.obj.createTextRange) { var range = this.obj.createTextRange(); range.moveStart('character',this.search_term.length); range.select(); } else { // var range = document.createRange(); // range.setStart(this.obj,this.search_term.length); this.obj.setSelectionRange(this.search_term.length,this.obj.value.length); } } } else { this.close_popup(); } /* update cache */ var found = false; var lc = this.search_term.toLowerCase(); for (i = 0; i < this.cache.length; i++) if (this.cache[i][0] == lc) { found = true; break; } if (!found) { this.cache[this.cache.length] = new Array(lc, results); } } /* update auto-compete input element with selected option */ AC.prototype.update_input = function() { this.obj.value = this.div.childNodes[this.selected_option].name; } /* when option is clicked with mouse, or entered with keyboard */ AC.prototype.onselected = function() { if (this.selected_option == null) if (this.cabbage == null) return; else this.selected_option = this.cabbage; /* opera funky */ this.update_input(); /* hide popup */ this.close_popup(); /* submit form */ if (this.submit_callback) this.submit_callback(); } /* capture up & down actions to prevent moving cursor left or right */ /* input.onkeypress() */ AC.prototype.onkeypress = function(e) { if (!e) e = window.event; var c = e.keyCode; if (c == 0) c = e.charCode; if(e.charCode) {_ac_key_check(this.AC,this.AC.typing_timeout); return;} switch (c) { case 38: /* up */ case 40: /* down */ e.returnValue = false; if (e.preventDefault) e.preventDefault(); break; default: break; } } /* move cursor on down to allow repeating */ /* input.onkeydown() */ AC.prototype.onkeydown = function(e) { if (!e) e = window.event; var c = e.keyCode; if (c == 0) c = e.charCode; var i = this.AC.selected_option == null ? -1 : this.AC.selected_option; if(e.charCode) {_ac_key_check(this.AC,this.AC.typing_timeout); return;} switch (c) { case 38: /* up */ i--; e.returnValue = false; if (e.preventDefault) e.preventDefault(); break; case 40: /* down */ i++; e.returnValue = false; if (e.preventDefault) e.preventDefault(); break; default: _ac_key_check(this.AC,this.AC.typing_timeout); break; } if (c == 38 || c == 40) { var length = this.AC.div.childNodes.length; if (i < 0) i = 0; if (i >= length) i = length-1; if (i != this.AC.selected_option) { for (j = 0; j < length; j++) { if (j == i) { this.AC.obj.value = this.AC.div.childNodes[j].value; this.AC.selected_option = this.AC.div.childNodes[j].divid; this.AC.div.childNodes[j].className = 'ac_highlight'; } else { this.AC.div.childNodes[j].className = 'ac_normal'; } } /* update search term */ this.AC.search_term = this.AC.div.childNodes[this.AC.selected_option].value; /* popup if hidden */ if (this.AC.div.style.visibility == 'hidden') { this.AC.suggest (this.AC.searched_term); } } } } /* input.onkeyup() */ AC.prototype.onkeyup = function(e) { if (!e) e = window.event; var c = e.keyCode; if (c == 0) c = e.charCode; switch (c) { /* prevent strange selections at top of option list */ case 38: /* up */ case 40: /* down */ e.returnValue = false; if (e.preventDefault) e.preventDefault(); break; /* select highlighted option */ case 13: /* enter */ this.AC.onselected(); e.returnValue = false; if (e.preventDefault) e.preventDefault(); break; /* hide popup window */ case 27: /* escape */ this.AC.close_popup(); e.returnValue = false; if (e.preventDefault) e.preventDefault(); break; /* get new suggestion for new data */ default: /* for latin this is ok: if (this.value.length > 0) { this.AC.searched_term = this.value; this.AC.suggest(this.value); } else { _ac_cancel(this.AC); this.AC.close_popup(); } */ break; } } /* iframe or XmlHttpRequest() callback */ function _ac_rpc() { var id = arguments[0]; if (_ac_map[id]) { /* we cannot shift() arguments as it is an object :( */ _ac_map[id].process_reply.apply(_ac_map[id],arguments); } } /* parse rpc results into html for the popup */ AC.prototype.process_reply = function() { var results = new Array(); var c = 0; var re = new RegExp('('+this.searched_term+')', "gi"); var nt = '$1'; for (i = 1; i < arguments.length; i += 2) { var name = this.highlight ? arguments[i+1].replace(re, nt) : arguments[i+1]; var value = this.highlight ? arguments[i].replace(re, nt) : arguments[i]; var html = ""+name+""+value+""; results[c] = new Array(arguments[i+1], arguments[i], c, html); c++; } this.update_popup(results); } function escapeURI(La){ if(encodeURIComponent) { return encodeURIComponent(La); } if(escape) { return escape(La) } }