FBJS/Examples/Typeahead/AJAX
From Facebook Developer Wiki
This is a modification of the Typeahead example, with support for an AJAX data source. --Tyson Malchow 21:11, 16 October 2007 (PDT)
This example works in Firefox - layout issues in IE7. Problems when multiple typeahead input boxes used on same page.
Contents |
[edit] Options
options = {
preMsgTxt: "type for suggestions", // text to display when nothing has been typed
menuOpacity: 94, // opacity of the menu
ajaxUrl: "http://yoursite.com/ajax_script.php", // url to your data source, must be absolute URL
focus: true, // whether or not to auto-focus the textbox upon creation
onEnter: function(event) { // handler for hitting the 'enter' key
alert('they hit enter!');
},
delayTime: 700, // amount of idle time after a keypress before making the ajax call
clearOnEnter: true // whether or not to clear the text after they hit enter
};
[edit] Back End Code Sample
The PHP script to handle the responses should probably be something like the following.
$entry = $_REQUEST['suggest_typed'];
$q = $dbc->query('SELECT `thing` FROM `table` WHERE `thing` LIKE "'.(mysql_real_escape_string($entry)).'%" ' .
'ORDER BY `thing` LIMIT 8');
$entries = array();
while($row = mysql_fetch_assoc($q))
$entries[] = '"'.addslashes($row['title']).'"';
echo '{fortext:"'.addslashes($entry).'",results:['.implode(',',$entries).']}';
// or use json_encode() if it's available on your system
The Ruby on Rails method to handle the responses should look like this... --ayn 13:22, 14 December 2007 (PST)
def auto_complete_for_table_thing
things = Table.find(:all,
:conditions => [ 'LOWER(thing) LIKE ?', params[:suggest_typed].downcase + '%'],
:order => 'thing ASC',
:limit => 10).map { |n| n.thing }
render :text => "{fortext:#{params[:suggest_typed].to_json},results:#{things.to_json}}"
end
[edit] Front End Code Sample
To create the suggest object, simply create a text box on your page like this.
<input id="i_like_text" autocomplete="off" maxlength="20" type="text"/><br />
Hint: Wrap the input in a div for better display
Then, follow it up with a javascript call (fastest if done in-page-body) with:
var suggestr = new ajaxSuggestFbml(document.getElementById('i_like_text'),options);
[edit] JavaScript
/**
written by tyson malchow for the Rate Me facebook application
*/
function ajaxSuggestFbml(obj, options) {
this.obj = obj;
// Setup the events we're listening to
this.obj.addEventListener('focus', this.onfocus.bind(this))
.addEventListener('blur', this.onblur.bind(this))
.addEventListener('keyup', this.onkeyup.bind(this))
.addEventListener('keydown', this.onkeydown.bind(this))
.addEventListener('keypress', this.onkeypress.bind(this));
// Create the dropdown list that contains our suggestions
this.list = document.createElement('div');
this.list.setClassName('suggest_list')
.setStyle({width: this.obj.getOffsetWidth()-2+'px',
display: 'none'});
this.obj.getParentNode().appendChild(this.list);
// Various flags
this.focused = true;
this.options = options == null ? {} : options;
this.selectedindex = -1;
this.delayTime = options.delayTime == null ? 700 : options.delayTime;
this.preMsgTxt = options.preMsgTxt == null ? "type for suggestions" : options.preMsgTxt;
this.normOpac = options.menuOpacity == null ? 94 : options.menuOpacity;
this.curOpac = this.normOpac;
this.cache = {};
this.obj.setStyle({margin: "0px"});
if(!options.focus) {
this.preMsg = true;
this.obj.setValue(this.preMsgTxt);
this.obj.addClassName("suggest_pretext");
} else {
this.obj.focus();
this.preMsg = false;
this.update_results();
this.show();
}
};
// Show suggestions when the user focuses the text field
ajaxSuggestFbml.prototype.onfocus = function(event) {
if(this.preMsg) {
this.obj.setValue("");
this.obj.removeClassName("suggest_pretext");
this.preMsg = false;
}
this.focused = true;
this.update_results();
this.obj.removeClassName('suggest_found');
this.show();
};
// ...and hide it when they leave the text field
ajaxSuggestFbml.prototype.onblur = function() {
if(this.obj.getValue() == "") {
this.preMsg = true;
this.obj.setValue(this.preMsgTxt);
this.obj.addClassName("suggest_pretext");
}
this.focused = false;
this.hide();
};
// Every keypress updates the suggestions
ajaxSuggestFbml.prototype.onkeyup = function(event) {
switch (event.keyCode) {
case 27: // escape
this.hide();
this.obj.removeClassName('suggest_found');
break;
case 13: // enter
if(this.options!=null && this.options.onEnter!=null) {
this.hide();
this.options.onEnter(event);
if(this.options.clearOnEnter == true) {
this.obj.setValue('');
this.hide();
this.obj.removeClassName('suggest_found');
}
}
break;
case 0:
case 37: // left
case 9: // tab
case 38: // up
case 39: // right
case 40: // down
break;
default:
this.selectedindex = -1;
this.update_results();
this.show();
this.obj.removeClassName('suggest_found');
break;
}
};
// We want interactive stuff to happen on keydown to make it feel snappy
ajaxSuggestFbml.prototype.onkeydown = function(event) {
switch (event.keyCode) {
case 9: // tab
case 13: // enter
if (this.results[this.selectedindex]) {
this.obj.addClassName('suggest_found')
.setValue(this.results[this.selectedindex]);
this.hide();
event.preventDefault();
}
break;
case 38: // up
this.select(this.selectedindex - 1);
event.preventDefault();
break;
case 40: // down
this.select(this.selectedindex + 1);
event.preventDefault();
break;
}
};
// Override these events so they don't actually do anything
ajaxSuggestFbml.prototype.onkeypress = function(event) {
switch (event.keyCode) {
case 13: // return
case 9: // tab
case 38: // up
case 40: // down
event.preventDefault();
break;
}
};
// Select a given index
ajaxSuggestFbml.prototype.select = function(index) {
var children = this.list.getChildNodes();
if(children != null && children.length > 0) {
if(index<0)
index = 0;
if(index >= children.length)
index = children.length - 1;
if(this.selectedindex >=0 && this.selectedindex < children.length )
children[this.selectedindex].removeClassName('suggest_selected');
children[(this.selectedindex=index)].addClassName('suggest_selected');
}
};
ajaxSuggestFbml.prototype.onajaxdone = function(data) {
// save to the cache
this.cache[data.fortext].results = data.results;
this.cache[data.fortext].curRequest = null;
// if its valid, update the UI
if(this.get_norm_typed() == data.fortext) {
this.draw_results(data.results, data.fortext);
this.results = data.results;
}
};
ajaxSuggestFbml.prototype.get_norm_typed = function() {
return this.obj.getValue().toLowerCase();
};
ajaxSuggestFbml.prototype.getValue = function() {
return (this.preMsg?"":this.obj.getValue());
};
ajaxSuggestFbml.prototype.draw_results = function(results, typed) {
this.list.setTextValue('');
if(results == null)
return;
for( var i = 0; i < results.length; i++ ) {
var item = document.createElement('div').setClassName('suggest_suggestion');
item.addEventListener('mouseover',
function() {
this[0].select(this[1]);
}.bind([this, i]));
item.addEventListener('mousedown',
function(event) {
this.obj.addClassName('suggest_found');
this.obj.setValue(this.results[this.selectedindex]);
this.hide();
}.bind(this));
var begins = results[i].toLowerCase().indexOf(typed);
if (begins == -1) {
var span_item = document.createElement('span').setTextValue(results[i]);
item.appendChild(span_item);
} else {
if (begins > 0) {
item.appendChild(document.createElement('span').setTextValue(results[i].substring(0, begins)));
}
var em_item = document.createElement('em').setTextValue(results[i].substring(begins, begins + typed.length));
item.appendChild(em_item);
var span_item = document.createElement('span').setTextValue(results[i].substring(begins + typed.length));
item.appendChild(span_item);
}
this.list.appendChild(item);
}
};
ajaxSuggestFbml.prototype.send_ajrequest = function(val) {
// ajax query
var request = new Ajax();
request.requireLogin = false;
request.responseType = Ajax.JSON;
request.onerror = function() {/* meh */};
request.ondone = this.onajaxdone.bind(this);
this.cache[val] = {curRequest: request};
this.cache[val].curRequest.post(this.options.ajaxUrl, {suggest_typed: val});
};
// This is called every keypress to update the suggestions
ajaxSuggestFbml.prototype.update_results = function() {
// Search the list of potential results and find ones that match what we have so far
var val = this.get_norm_typed();
if(this.cache[val] != null) {
// pull from el cache
this.draw_results(this.cache[val].results,val);
this.results = this.cache[val].results;
} else if(this.cache[val] == null || this.cache[val].curRequest == null){
if(this.requestTimer != null)
clearTimeout(this.requestTimer);
this.requestTimer = setTimeout(
function() {
this.send_ajrequest(val);
this.requestTimer = null;
}.bind(this), this.delayTime);
}
};
ajaxSuggestFbml.prototype.cleanup = function() {
this.hide();
this.obj.setValue('');
this.obj.removeClassName('suggest_found');
}
ajaxSuggestFbml.prototype.show = function() {
this.list.setStyle('display', 'block');
};
ajaxSuggestFbml.prototype.hide = function() {
this.selectedindex = -1;
this.list.setStyle('display', 'none');
};
[edit] CSS
.suggest_placeholder {
color: #777;
}
.suggest_found {
background: #e1e9f6;
}
.suggest_pretext {
color: #aaa;
}
.suggest_list {
background: transparent;
border: 1px solid #bdc7d8;
border-top: none;
font-size: 11px;
margin-top: -1px;
overflow: hidden;
position: absolute;
text-align: left;
z-index: 102;
}
.suggest_list .suggest_suggestion {
background: #fff;
border-top: 1px solid #ddd;
color: #000;
cursor: pointer;
padding: 3px;
opacity: 0.94;
width: 100%;
}
.suggest_list .suggest_suggestion em {
background: #d8dfea;
color: black;
font-style: normal;
font-weight: bold;
}
.suggest_list .suggest_suggestion small {
color: #808080;
padding-left: 5px;
}
.suggest_list .suggest_selected {
background: #3b5998;
color: #fff;
opacity: 1;
}
.suggest_list .suggest_selected em {
background: #5670a6;
color: #fff;
}
[edit] Notes
- If you're having trouble with IE, add <br /> to end of the input field.
