File: //old_home_backup/gurkhajustice.org.uk/public_html/script/lib/autocomplete.js
/**
* Author and Version Information {{{
* author: Antonio Ramirez http://http://webeaters.blogspot.com
*
* class: AutoComplete for Prototype 1.6.0
*
* version: 1.2.1 - 2007-11-11
* (based on AutoSuggest 2.1.3 - 2007-07-19)
* version: 1.3.0 - 2008-01-03 by Andrew Nicols <andrew@nicols.co.uk>
* - Fixed incorrect title-casing - CSS is Case Sensitive!!!
* - Adjusted the way in which the Notifier images are loaded.
* - Changed json code to pass all json variables back instead of just id, value and name
* - Fixed 'GMAIL' code such that if valueSep is undefined, it is ignored
* - Changed the default for valueSep to null
* - Fixed the resetTimeout function
*
* REFERENCES AND THANKS
* this class is based on the work in AutoSuggest.js of
* Timothy Groves - http://http://www.brandspankingnew.net
* and adapted for use with prototype 1.6.0
*
* UPDATED by R��da HADJOUTI
* GMAIL like AutoComplete (semicolon separator) Update
*
}}}*/
var AutoComplete = Class.create();
AutoComplete.prototype = { // {{{
Version: '1.3.0',
REQUIRED_PROTOTYPE: '1.6.0',
initialize: function (id, param) { // {{{
// check whether we have the appropiate javascript libraries
this.PROTOTYPE_CHECK();
// Get the field we're watching.
// It needs to be a valid field so throw an error if it's not valid or can't be found.
this.fld = $(id);
if (!this.fld)
{
throw("AutoComplete requires a field id to initialize");
}
// Init variables
this.sInp = ""; // input value
this.nInpC = 0; // input value length
this.aSug = []; // suggestions array
this.iHigh = 0; // level of list selection
// Parameter Handling {{{
// Set the use specified options
this.options = param ? param : {};
// These are the default settings {{{
var k, def = {
valueSep:null,
minchars:1,
meth:"get",
varname:"input",
className:"autocomplete",
timeout:3000,
delay:500,
offsety:-5,
shownoresults: true,
noresults: "No results were found.",
maxheight: 250,
cache: true,
maxentries: 25,
onAjaxError:null,
setWidth: true,
minWidth: 180,
maxWidth: 380,
useNotifier: true,
autoSubmit: false,
autoSubmitForm: ""
};
//}}}
// Overlay any values which weren't user specified.
for (k in def)
{
if (typeof(this.options[k]) != typeof(def[k]))
this.options[k] = def[k];
}
// End of Parameter Handling }}}
// Not everyone wants to use the Notifier. Give them the option
if (this.options.useNotifier)
{
this.fld.addClassName('ac_field');
}
// set keyup handler for field
// and prevent AutoComplete from client
var p = this;
// NOTE: not using addEventListener because UpArrow fired twice in Safari
this.fld.onkeypress = function(ev){ return p.onKeyPress(ev); };
this.fld.onkeyup = function(ev){ return p.onKeyUp(ev); };
// ARN-DEBUG Chances are we want to reset the timeout when they lose focus, at least that's what I prefer
this.fld.onblur = function(ev){ p.resetTimeout(); return true; };
// ARN-DEBUG Not sure what this is about!
this.fld.setAttribute("AutoComplete","off");
}, //}}}
convertVersionString: function (versionString){ // {{{
var r = versionString.split('.');
return parseInt(r[0])*100000 + parseInt(r[1])*1000 + parseInt(r[2]);
}, // }}}
PROTOTYPE_CHECK: function() { // {{{
if((typeof Prototype=='undefined') ||
(typeof Element == 'undefined') ||
(typeof Element.Methods=='undefined') ||
(this.convertVersionString(Prototype.Version) <
this.convertVersionString(this.REQUIRED_PROTOTYPE)))
throw("AutoComplete requires the Prototype JavaScript framework >= " +
this.REQUIRED_PROTOTYPE);
}, // }}}
// set responses to keypress events in the field
// this allows the user to use the arrow keys to scroll through the results
// ESCAPE clears the list
// RETURN sets the current highlighted value
// UP/DOWN move around the list
onKeyPress: function (e) { // {{{
if (!e) e = window.event;
var key = e.keyCode || e.wich;
switch(key)
{
case Event.KEY_RETURN:
this.setHighlightedValue();
Event.stop(e);
break;
case Event.KEY_TAB:
this.setHighlightedValue();
//Event.stop(e);
break;
case Event.KEY_ESC:
this.clearSuggestions();
break;
}
return true;
}, //}}}
onKeyUp: function (e) { // {{{
if (!e) e = window.event;
var key = e.keyCode || e.wich;
if (key == Event.KEY_UP || key == Event.KEY_DOWN)
{
this.changeHighlight(key);
Event.stop(e);
}
else this.getSuggestions(this.fld.value);
return true;
}, //}}}
getSuggestions: function(val) { // {{{
// input the same? do nothing
if(val==this.sInp) return false;
// kill the old list
if($(this.acID)) $(this.acID).remove();
this.sInp = val;
// input length is less than the min required to trigger a request
// do nothing
if (val.length < this.options.minchars)
{
this.aSug = [];
this.nInpC = val.length;
return false;
}
// Here we will detect if there is a comma and the splitted value has a value to check
// comma stars a new search and val is converted to the new value after the comma
var ol = this.nInpC; // old length
this.nInpC = val.length ? val.length : 0;
// if caching enabled, and we didn't receive the maxentries value
// and user is typing (ie. length of input is increasing)
// filter results out of suggestions from last request
var l = this.aSug.length;
if( this.options.cache && ( this.nInpC > ol ) && l && ( l < this.options.maxentries ) )
{
var arr = new Array();
for (var i=0;i<l;i++) {
if (this.aSug[i].value.toLowerCase().indexOf(val.toLowerCase()) != -1)
{
arr.push(this.aSug[i]);
}
}
this.aSug = arr;
// recreate the list
this.createList(this.aSug);
} else {
// do new request
var p = this;
//http://var input = this.sInp; // send the converted new value (comma)
clearTimeout(this.ajID); // ajax id timer
this.ajID = setTimeout( function () {p.doAjaxRequest(p.sInp)}, this.options.delay);
}
document.helper = this;
return false;
}, // }}}
getLastInput : function(str) { // {{{
var ret = str;
if (undefined != this.options.valueSep) {
var idx = ret.lastIndexOf(this.options.valueSep);
ret = idx == -1 ? ret : ret.substring(idx + 1, ret.length);
}
return ret;
}, // }}}
doAjaxRequest: function (input) { // {{{
// we have to check here if there is a new splitted value (, or ;)
// always check against the last part of the comma and then check
// saved input is still the value of the field
if (input != this.fld.value)
return false;
// Gmail like : get only the last user's input
this.sInp = this.getLastInput(this.sInp);
// create ajax request
// do we need to call a function to recreate the url?
if (typeof this.options.script == 'function')
var url = this.options.script(encodeURIComponent(this.sInp));
else
var url = this.options.script+this.options.varname+'='+encodeURIComponent(this.sInp);
if(!url) return false;
var p = this;
var m = this.options.meth; // get or post?
if( this.options.useNotifier )
{
this.fld.removeClassName('ac_field');
this.fld.addClassName('ac_field_busy');
};
var options = {
method: m,
onSuccess: function (req) { // {{{
if( p.options.useNotifier )
{
p.fld.removeClassName('ac_field_busy');
p.fld.addClassName('ac_field');
};
p.setSuggestions(req,input);
}, // }}}
onFailure: (typeof p.options.onAjaxError == 'function')? function (status) { // {{{
if (p.options.useNotifier)
{
p.fld.removeClassName('ac_field_busy');
p.fld.addClassName('ac_field');
}
p.options.onAjaxError(status)
} : // }}}
function (status) { // {{{
if (p.options.useNotifier)
{
p.fld.removeClassName('ac_field_busy');
p.fld.addClassName('ac_field');
}
alert("AJAX error: "+status);
} // }}}
}
// make new ajax request
new Ajax.Request(url, options);
}, // }}}
setSuggestions: function (req, input) { // {{{
// if field input no longer matches what was passed to the request
// don't show the suggestions
// here we need to check against the splitted values if any (, or ;)
if (input != this.fld.value)
return false;
this.aSug = [];
if(this.options.json)
{ // response in json format?
var jsondata = eval('(' + req.responseText + ')');
this.aSug = jsondata.results;
} else {
// response in xml format?
var results = req.responseXML.getElementsByTagName('results')[0].childNodes;
for(var i=0;i<results.length;i++)
{
if(results[i].hasChildNodes())
this.aSug.push( { 'id':results[i].getAttribute('id'), 'value':results[i].childNodes[0].nodeValue, 'info':results[i].getAttribute('info') } );
}
}
this.acID = 'ac_'+this.fld.id;
this.createList(this.aSug);
}, // }}}
createDOMElement: function ( type, attr, cont, html ) { // {{{
var ne = document.createElement( type );
if (!ne)
return 0;
for (var a in attr)
ne[a] = attr[a];
var t = typeof(cont);
if (t == "string" && !html)
ne.appendChild( document.createTextNode(cont) );
else if (t == "string" && html)
ne.innerHTML = cont;
else if (t == "object")
ne.appendChild( cont );
return ne;
}, // }}}
createList: function(arr) { // {{{
// get rid of the old list if any
if($(this.acID)) $(this.acID).remove();
// clear list removal timeout
this.killTimeout();
// if no results, and showNoResults is false, do nothing
if (arr.length == 0 && !this.options.shownoresults) return false;
// create holding div
var div = this.createDOMElement('div', {id:this.acID, className:this.options.className});
// create div header
var acpointer = this.createDOMElement('div', {className: 'ac_pointer'});
var hcorner = this.createDOMElement('div', {className: 'ac_corner'});
var hbar = this.createDOMElement('div', {className: 'ac_bar'});
var header = this.createDOMElement('div', {className: 'ac_header'});
//http://Rajesh Testing
var myContainer = this.createDOMElement('div', {className: 'mycontainer'});
/*----*/
header.appendChild(hcorner);
header.appendChild(hbar);
div.appendChild(acpointer);
div.appendChild(header);
div.appendChild(myContainer);
// create and populate ul
var ul = this.createDOMElement('ul', {id:'ac_ul'});
var p = this; // pointer that we will need later on
// no results?
if (arr.length == 0 && this.options.shownoresults)
{
var li = this.createDOMElement('li', {className: 'ac_warning'}, this.options.noresults );
ul.appendChild(li);
} else {
// loop through arr of suggestions creating an LI element for each of them
for (var i=0,l = arr.length; i<l; i++)
{
// format output with the input enclosed in a EM elementFromPoint
// (as HTML not DOM)
var val = arr[i].value;
var st = val.toLowerCase().indexOf(this.sInp.toLowerCase()); // HERE WE CHECK AGAINST THE SPLITTED VALUE IF ANY***
var output = val.substring(0,st) + '<em>' + val.substring(st,st+this.sInp.length) + '</em>' + val.substring(st+this.sInp.length);
var span = this.createDOMElement('span',{},output,true); // type of, properties, output, isHTML?
if(arr[i].info != '') // do we need to add extra info?
{
var br = this.createDOMElement('br',{});
span.appendChild(br);
var small = this.createDOMElement('small',{}, arr[i].info);
span.appendChild(small);
}
var a = this.createDOMElement('a',{href:'#'});
var tl = this.createDOMElement('span',{className:'tl'},' ',true);
var tr = this.createDOMElement('span',{className:'tr'},' ',true);
a.appendChild(tl);
a.appendChild(tr);
a.appendChild(span); // add the object span into the link
a.name = i+1;
a.onclick = function () { // {{{
p.setHighlightedValue();
return false;
}; // }}}
a.onmouseover = function () { // {{{
p.setHighlight(this.name);
}; // }}}
var li = this.createDOMElement('li', {}, a); // add the link element to a li element
// finally add the newly created li element to the ul element
ul.appendChild(li);
}
}
//http://Rajesh Commented this - div.appendChild(ul); // add the newly created list to the div element
myContainer.appendChild(ul);
// create div footer
var fcorner = this.createDOMElement('div', {className: 'ac_corner'});
var fbar = this.createDOMElement('div', {className: 'ac_bar'});
var footer = this.createDOMElement('div', {className: 'ac_footer'});
footer.appendChild(fcorner);
footer.appendChild(fbar);
div.appendChild(footer);
// get position of target textfield
// position holding div below it
// set width of holding div to width of field
// if
var pos = this.fld.cumulativeOffset();
div.style.left = pos[0] + "px";
div.style.top = pos[1] + this.fld.offsetHeight + "px";
var w =
(
this.options.setWidth && this.fld.offsetWidth < this.options.minWidth
)
? this.options.minWidth :
(
this.options.setWidth && this.fld.offsetWidth > this.options.maxWidth
)
? this.options.maxWidth :
this.fld.offsetWidth;
div.style.width = w + "px";
// set mouseover functions for div
// when mouse pointer leaves div, set a timeout to remove the list after an interval
// when mouse enters div, kill the timeout so the list won't be removed
//
div.onmouseover = function(){ p.killTimeout() };
div.onmouseout = function(){ p.resetTimeout() };
// add DIV to document
document.getElementsByTagName("body")[0].appendChild(div);
// highlight first item
this.iHigh = 1;
this.setHighlight(1);
// remove list after interval
this.toID = setTimeout(
function () {
p.clearSuggestions()
}, this.options.timeout
);
}, // }}}
changeHighlight: function(key) { // {{{
var list = $("ac_ul");
if (!list)
return false;
var n;
n = (key == Event.KEY_DOWN || key == Event.KEY_TAB)? this.iHigh + 1 : this.iHigh - 1; // false assumed to be Event.KEY_UP
n = (n > list.childNodes.length)? list.childNodes.length : ((n < 1)? 1 : n);
this.setHighlight(n);
}, // }}}
setHighlight: function(n) { // {{{
var list = $('ac_ul');
if (!list) return false;
if (this.iHigh > 0) this.clearHighlight();
this.iHigh = Number(n);
list.childNodes[this.iHigh-1].className = 'ac_highlight';
this.killTimeout();
}, // }}}
clearHighlight: function() { // {{{
var list = $('ac_ul');
if(!list) return false;
if(this.iHigh > 0)
{
list.childNodes[this.iHigh-1].className = '';
this.iHigh = 0;
}
}, // }}}
setHighlightedValue: function() { // {{{
if (this.iHigh)
{
// HERE WE NEED TO IMPLEMENT THE GMAIL LIKE SPLITTED VALUE
if (!this.aSug[this.iHigh - 1]) return;
// Gmail like
if (undefined != this.options.valueSep) {
var str = this.getLastInput(this.fld.value);
var idx = this.fld.value.lastIndexOf(str);
str = this.aSug[ this.iHigh -1 ].value + this.options.valueSep;
this.sInp = this.fld.value = idx == -1 ? str : this.fld.value.substring(0, idx) + str;
} else {
var str = this.getLastInput(this.fld.value);
var idx = this.fld.value.lastIndexOf(str);
str = this.aSug[ this.iHigh -1 ].value;
this.sInp = this.fld.value = idx == -1 ? str : this.fld.value.substring(0, idx) + str;
}
// move cursor to end of input (safari)
this.fld.focus();
if(this.fld.selectionStart)
this.fld.setSelectionRange(this.sInp.length, this.sInp.length);
this.clearSuggestions();
// pass selected object to callback function, if exists
if (typeof this.options.callback == 'function')
this.options.callback(this.aSug[this.iHigh-1]); // the object has the properties we want, it will depend of
}
if(this.options.autoSubmit){
subForm = $(this.options.autoSubmitForm);
if(subForm) subForm.submit();
}
//alert(this.options['autoSubmit']);
//alert(this.options['autoSubmitForm']);
}, // }}}
killTimeout: function() { // {{{
clearTimeout(this.toID);
}, // }}}
resetTimeout: function() { // {{{
this.killTimeout();
var p = this;
this.toID = setTimeout(
function () {
p.clearSuggestions();
}, p.options.timeout
);
// ARN-DEBUG Added p.options.timeout back :|
}, // }}}
clearSuggestions: function () { // {{{
this.killTimeout();
if ($(this.acID))
{
this.fadeOut(300,function () {
$(this.acID).remove();
} );
}
}, // }}}
fadeOut: function (milliseconds, callback) { // {{{
this._fadeFrom = 1;
this._fadeTo = 0;
this._afterUpdateInternal = callback;
this._fadeDuration = milliseconds;
this._fadeInterval = 50;
this._fadeTime = 0;
var p = this;
this._fadeIntervalID = setInterval(
function() {
p._changeOpacity()
}, this._fadeInterval
);
}, // }}}
_changeOpacity: function() { // {{{
if (!$(this.acID))
{
this._fadeIntervalID=clearInterval(this._fadeIntervalID);
return;
}
this._fadeTime += this._fadeInterval;
var ieop = Math.round( (this._fadeFrom + ((this._fadeTo - this._fadeFrom) * (this._fadeTime/this._fadeDuration))) * 100)
var op = ieop / 100;
var el = $(this.acID);
if (el.filters) // internet explorer
{
try {
el.filters.item("DXImageTransform.Microsoft.Alpha").opacity = ieop;
} catch (e) {
// If it is not set initially, the browser will throw an error.
// This will set it if it is not set yet.
el.style.filter = 'progid:DXImageTransform.Microsoft.Alpha(opacity='+ieop+')';
}
} else {
el.style.opacity = op;
}
if (this._fadeTime >= this._fadeDuration)
{
clearInterval( this._fadeIntervalID );
if (typeof this._afterUpdateInternal == 'function')
this._afterUpdateInternal();
}
} // }}}
} // }}}
// vim: set filetype=javascript foldmethod=marker foldlevel=5: