| We use an autocompleter.js library downloaded off the net and have a taf action that provides the search. Here's the library: ________________________________________________________________________ TO UNSUBSCRIBE: Go to http://www.witango.com/developer/maillist.taf |
//
// This is a port of the sciptaculous autocomplete which is distributed under the MIT license.
//
// Autocompleter.Base handles all the autocompletion functionality
// Autocompleter.Base handles all the autocompletion functionality
// that's independent of the data source for autocompletion. This
// includes drawing the autocompletion menu, observing keyboard
// and mouse events, and similar.
//
// Specific autocompleters need to provide, at the very least,
// a getUpdatedChoices function that will be invoked every time
// the text inside the monitored textbox changes. This method
// should get the text for which to provide autocompletion by
// invoking this.getToken(), NOT by directly accessing
// this.element.value. This is to allow incremental tokenized
// autocompletion. Specific auto-completion logic (AJAX, etc)
// belongs in getUpdatedChoices.
//
// Tokenized incremental autocompletion is enabled automatically
// when an autocompleter is instantiated with the 'tokens' option
// in the options parameter, e.g.:
// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
// will incrementally autocomplete with a comma as the token.
// Additionally, ',' in the above example can be replaced with
// a token array, e.g. { tokens: [',', '\n'] } which
// enables autocompletion on multiple tokens. This is most
// useful when one of the tokens is \n (a newline), as it
// allows smart autocompletion after linebreaks.
var KEY_BACKSPACE = 8
var KEY_TAB = 9
var KEY_RETURN = 13
var KEY_ESC = 27
var KEY_LEFT = 37
var KEY_UP = 38
var KEY_RIGHT = 39
var KEY_DOWN = 40
var KEY_DELETE = 46
Autocompleter = function() {
}
Autocompleter.Base = function() {
}
update(Autocompleter.Base.prototype, {
baseInitialize: function(element, update, options) {
this.element = $(element);
this.update = $(update);
this.hasFocus = false;
this.changed = false;
this.active = false;
this.index = 0;
this.entryCount = 0;
if (this.setOptions)
this.setOptions(options);
else
this.options = options || {};
this.options.paramName = this.options.paramName || this.element.name;
this.options.tokens = this.options.tokens || [];
this.options.frequency = this.options.frequency || 0.4;
this.options.minChars = this.options.minChars || 1;
this.options.onShow = this.options.onShow ||
bind(function(element, update){
if(!update.style.position || update.style.position=='absolute') {
update.style.position = 'absolute';
this.clonePosition(element, update);
//Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight});
}
// Todo: make it fade in ?
showElement(update);
//Effect.Appear(update,{duration:0.15});
}, this);
this.options.onHide = this.options.onHide ||
function(element, update){
// Todo: make if fade out like in the scriptaculous version?
//new Effect.Fade(update,{duration:0.15})
hideElement(update);
};
if (typeof(this.options.tokens) == 'string')
this.options.tokens = new Array(this.options.tokens);
this.observer = null;
this.element.setAttribute('autocomplete','off');
hideElement(this.update);
addToCallStack(this.element, "onblur", bind(this.onBlur, this));
addToCallStack(this.element, "onkeypress", bind(this.onKeyPress, this));
//Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
//Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
},
/**
* Copied from prototype.js version 1.4.0_rc3
*/
cumulativeOffset: function(element) {
var valueT = 0, valueL = 0;
do {
valueT += element.offsetTop || 0;
valueL += element.offsetLeft || 0;
element = element.offsetParent;
} while (element);
return [valueL, valueT];
},
/**
* Copied from prototype.js version 1.4.0_rc3
*/
clonePosition: function(source, target) {
source = $(source);
target = $(target);
//target.style.position = 'absolute';
var offsets = this.cumulativeOffset(source);
//target.style.top = offsets[1] + 'px';
target.style.left = offsets[0] + 'px';
target.style.width = source.offsetWidth + 'px';
//target.style.height = source.offsetHeight + 'px';
},
show: function() {
if(this.update.style.display == 'none') this.options.onShow(this.element, this.update);
/*
if(!this.iefix &&
(navigator.appVersion.indexOf('MSIE')>0) &&
(navigator.userAgent.indexOf('Opera')<0) &&
(this.update.style.position=='absolute')) {
appendChildNodes(this.update,
'<iframe id="' + this.update.id + '_iefix" '+
'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
this.iefix = $(this.update.id+'_iefix');
}
if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
*/
},
fixIEOverlapping: function() {
this.clonePosition(this.update, this.iefix);
this.iefix.style.zIndex = 1;
this.update.style.zIndex = 2;
showElement(this.iefix);
},
hide: function() {
this.stopIndicator();
if(this.update.style.display !='none') this.options.onHide(this.element, this.update);
if(this.iefix) hideElement(this.iefix);
},
startIndicator: function() {
if(this.options.indicator) showElement(this.options.indicator);
},
stopIndicator: function() {
if(this.options.indicator) hideElement(this.options.indicator);
},
onKeyPress: function(event) {
if( event == null )
event = window.event;
if(this.active)
switch(event.keyCode) {
case KEY_TAB:
case KEY_RETURN:
this.selectEntry();
this.stopEvent(event);
case KEY_ESC:
this.hide();
this.active = false;
this.stopEvent(event);
return;
case KEY_LEFT:
case KEY_RIGHT:
return;
case KEY_UP:
this.markPrevious();
this.render();
if(navigator.appVersion.indexOf('AppleWebKit')>0) this.stopEvent(event);
return;
case KEY_DOWN:
this.markNext();
this.render();
if(navigator.appVersion.indexOf('AppleWebKit')>0) this.stopEvent(event);
return;
}
else
if(event.keyCode==KEY_TAB || event.keyCode==KEY_RETURN)
return;
this.changed = true;
this.hasFocus = true;
if(this.observer) clearTimeout(this.observer);
this.observer =
setTimeout(bind(this.onObserverEvent, this), this.options.frequency*1000);
},
onHover: function(event) {
var element = this.findElement(event, 'LI');
if(this.index != element.autocompleteIndex)
{
this.index = element.autocompleteIndex;
this.render();
}
this.stopEvent(event);
},
// find the first node with the given tagName, starting from the
// node the event was triggered on; traverses the DOM upwards
findElement: function(event, tagName) {
if( event == null ) event = window.event;
var element = event.target || event.srcElement;
while (element.parentNode && (!element.tagName ||
(element.tagName.toUpperCase() != tagName.toUpperCase())))
element = element.parentNode;
return element;
},
stopEvent: function(event) {
if( event == null ) event = window.event;
if (event.preventDefault) {
event.preventDefault();
event.stopPropagation();
} else {
event.returnValue = false;
event.cancelBubble = true;
}
},
onClick: function(event) {
var element = this.findElement(event, 'LI');
this.index = element.autocompleteIndex;
this.selectEntry();
this.hide();
},
onBlur: function(event) {
// needed to make click events working
setTimeout(bind(this.hide, this), 250);
this.hasFocus = false;
this.active = false;
},
render: function() {
if(this.entryCount > 0) {
for (var i = 0; i < this.entryCount; i++)
this.index==i ?
addElementClass(this.getEntry(i),"selected") :
removeElementClass(this.getEntry(i),"selected");
if(this.hasFocus) {
this.show();
this.active = true;
}
} else {
this.active = false;
this.hide();
}
},
markPrevious: function() {
if(this.index > 0) this.index--
else this.index = this.entryCount-1;
},
markNext: function() {
if(this.index < this.entryCount-1) this.index++
else this.index = 0;
},
getEntry: function(index) {
return this.update.firstChild.childNodes[index];
},
getCurrentEntry: function() {
return this.getEntry(this.index);
},
selectEntry: function() {
this.active = false;
this.updateElement(this.getCurrentEntry());
},
updateElement: function(selectedElement) {
if (this.options.updateElement) {
this.options.updateElement(selectedElement);
return;
}
// the reverse, join is a workaround for a bug in the 1.1 version of MochiKit
//
var value = scrapeText(selectedElement, true).reverse().join("");
var lastTokenPos = this.findLastToken();
if (lastTokenPos != -1) {
var newValue = this.element.value.substr(0, lastTokenPos + 1);
var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
if (whitespace)
newValue += whitespace[0];
this.element.value = newValue + value;
} else {
this.element.value = value;
}
this.element.focus();
if (this.options.afterUpdateElement)
this.options.afterUpdateElement(this.element, selectedElement);
},
updateChoices: function(choices) {
if(!this.changed && this.hasFocus) {
this.update.innerHTML = choices;
// TODO: implement this via MochiKit
//Element.cleanWhitespace(this.update);
//Element.cleanWhitespace(this.update.firstChild);
if(this.update.firstChild && this.update.firstChild.childNodes) {
this.entryCount =
this.update.firstChild.childNodes.length;
for (var i = 0; i < this.entryCount; i++) {
var entry = this.getEntry(i);
entry.autocompleteIndex = i;
this.addObservers(entry);
}
} else {
this.entryCount = 0;
}
this.stopIndicator();
this.index = 0;
this.render();
}
},
addObservers: function(element) {
addToCallStack(element, "onmouseover", bind(this.onHover, this));
addToCallStack(element, "onclick", bind(this.onClick, this));
},
onObserverEvent: function() {
this.changed = false;
if(this.getToken().length>=this.options.minChars) {
this.startIndicator();
this.getUpdatedChoices();
} else {
this.active = false;
this.hide();
}
},
getToken: function() {
var tokenPos = this.findLastToken();
if (tokenPos != -1)
var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
else
var ret = this.element.value;
return /\n/.test(ret) ? '' : ret;
},
findLastToken: function() {
var lastTokenPos = -1;
for (var i=0; i<this.options.tokens.length; i++) {
var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
if (thisTokenPos > lastTokenPos)
lastTokenPos = thisTokenPos;
}
return lastTokenPos;
}
}
);
Autocompleter.Ajax = function() {
}
update(Autocompleter.Ajax.prototype, Autocompleter.Base.prototype, {
init: function(element, update, url, options) {
this.baseInitialize(element, update, options);
this.options.asynchronous = true;
this.options.defaultParams = this.options.parameters || null;
this.url = url;
},
getUpdatedChoices: function() {
entry = encodeURIComponent(this.options.paramName) + '=' +
encodeURIComponent(this.getToken());
this.options.parameters = this.options.callback ?
this.options.callback(this.element, entry) : entry;
if(this.options.defaultParams)
this.options.parameters += '&' + this.options.defaultParams;
var url = this.url + '?' + this.options.parameters;
var deferred = doSimpleXMLHttpRequest(url);
deferred.addCallback( bind(this.onComplete, this) );
deferred.addErrback( function(err) { alert("xmlhttp error:" + err); });
//new Ajax.Request(this.url, this.options);
},
onComplete: function(request) {
this.updateChoices(request.responseText);
}
});
You need to point to a search function that returns the data as a <ul> list. example <ul class="autocompleter"> <@rows><li><@col 1><span class="informal"><@ifequal '<@col 2>' 'N'> [PubMed Journal]</@if></span></li></@rows> </ul> (Note that in this case we get the argument, massage it a bit to clean it up, and then perform a case insensitive Oracle search. You can have some really complex stuff happen in this step behind the scenes.) Anything you put between <span class="informal"> will show in the displayed list, but will NOT be passed as an argument. What I have not figured out how to do yet is to show one value and pass a different one, which I would really like to do in some cases. And then put it into a tcf (entire code shown below between the <pre> tags so it won't show up as spam ), then made it into a custom tag we can call for any autocompletes... Witango rocks when it comes to Ajax-ery. Note the obvious, that you have to call the library in the document's head section. <pre> <@ifempty <@var method$size>><@assign name="method$size" value="20"></@if> <@ifempty <@var method$TAF>><@assign name="method$taf" value="<@cgi><@appfile>"></@if> <input type="text" name="<@var method$fieldname>" id="<@var method$fieldname>" autocomplete="off" size="<@var method$size>"> <div id="<@var method$fieldname>hint" style="display: none;"></div> <@if "('<@var method$function>' = '') and ('<@var method$args>' = '')"> <script type="text/_javascript_"> new Ajax.Autocompleter("<@var method$fieldname>","<@var method$fieldname>hint","<@var method$TAF>?"); </script> <@elseif "('<@var method$function>' != '') and ('<@var method$args>' = '')"> <script type="text/_javascript_"> new Ajax.Autocompleter("<@var method$fieldname>","<@var method$fieldname>hint","<@var method$TAF>?_function=<@var method$function>"); </script> <@elseif "('<@var method$function>' = '') and ('<@var method$args>' != '')"> <script type="text/_javascript_"> new Ajax.Autocompleter("<@var method$fieldname>","<@var method$fieldname>hint","<@var method$TAF>?<@var method$args>"); </script> <@else> <script type="text/_javascript_"> new Ajax.Autocompleter("<@var method$fieldname>","<@var method$fieldname>hint","<@var method$TAF>?_function=<@var method$function>&<@var method$args>"); </script> </@if> </pre> Call it with this format, where the custom tag is <@ajax_completer <@ajax_completer fieldname="" TAF="" function="" args="" size=""> where all except fieldname can be null - if this is a function in the same taf, taf should be null. If function is null, ditto. Size specifies the size of the input field, default is 20. It helps to tweak styles for this, by the way. The data is output as a list, and is transparent without styles and really, really hard to read over other text. So if I set <@ajax_completer fieldname="journal" taf="journalfind.taf" size="50">, what would occur on the html page would be (less the <pre> tags!) <pre> <input type="text" name="journal" id="journal" autocomplete="off" size="50"> <div id="journalhint" style="display: none;"></div> <script type="text/_javascript_"> new Ajax.Autocompleter("journal","journalhint","journalfind.taf?"); </script> </pre> On Apr 11, 2007, at 2:15 PM, Stefan Gonick wrote: Hi Everyone, |
