cacert-webdb/www/ac.js

548 lines
15 KiB
JavaScript
Raw Normal View History

2005-12-04 21:04:05 +00:00
/* vim:ts=4:sts=4:sw=2:noai:noexpandtab
*
* Auto-complete client side javascript.
* Copyright (c) 2005 Steven McCoy <fnjordy@gmail.com>
*
* 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(<type>, <id>, <submit callback>)
* AC(<type>, <id>)
* AC(<id>)
*/
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 = '<font color="red"><b>$1</b></font>';
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 = "<span class='d'>"+name+"</span><span class='a'>"+value+"</span>";
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)
}
}