Triggered @mention Autocomplete like Facebook, Twitter & Google+

Platform:  jQuery
Published  Mar 22, 2012
Updated  Feb 11, 2013

Screenshots

This widget lets you search for users to @mention in your posts. It works very much like Facebook and Google+ in that it supports users with spaces in their name. It writes to a hidden field with the user ID's formatted in this way: @ [12345] while showing @username in the input box. You can save the encoded string for easier parsing at display time. $('#inputbox').triggeredAutocomplete({
hidden: '#hidden_inputbox',
source: "/search.php",
trigger: "@"
});
You can use a predefined array or json as a source. Example json result: [{"value":"1234","label":"Beef"},{"value":"98765","label":"Chicken"}]To use the hidden field without an ajax call you need to pass an associative array: $('#inputbox').triggeredAutocomplete({
hidden: '#hidden_inputbox,
source: new Array({ "value": "1234", "label": 'Geech'}, {"value": "5312", "label": "Marf"})
});
This also supports an optional img to appear beside each result. You just need to pass an img URL for each value and label. Here is the CSS for the image: .ui-menu-item img { padding-right: 10px; width: 32px; height: 32px; }
.ui-menu-item span { color: #444; font-size: 12px; vertical-align: top }
If you want editable posts, you need to pass an id_map as an attr tag of the input box. This is also json encoded and is simply an associative array of the included user_id => username pairs in the existing post. This is so when you change the post the original @mentions are preserved in their @ [12345] format.

Live Demo:
http://jsfiddle.net/vq6MH/146/

GitHub repo:
https://github.com/Hawkers/triggeredAutocomplete

Node.js server side component:
http://www.hawkee.com/snippet/9487/ /********************************************************************************
/*
* triggeredAutocomplete (jQuery UI autocomplete widget)
* 2012 by Hawkee.com (hawkee@gmail.com)
*
* Version 1.4.5
*
* Requires jQuery 1.7 and jQuery UI 1.8
*
* Dual licensed under MIT or GPLv2 licenses
* http://en.wikipedia.org/wiki/MIT_License
* http://en.wikipedia.org/wiki/GNU_General_Public_License
*
*/

;(function ( $, window, document, undefined ) {
$.widget("ui.triggeredAutocomplete", $.extend(true, {}, $.ui.autocomplete.prototype, {

options: {
trigger: "@",
allowDuplicates: true
},

_create:function() {

var self = this;
this.id_map = new Object();
this.stopIndex = -1;
this.stopLength = -1;
this.contents = '';
this.cursorPos = 0;

/** Fixes some events improperly handled by ui.autocomplete */
this.element.bind('keydown.autocomplete.fix', function (e) {
switch (e.keyCode) {
case $.ui.keyCode.ESCAPE:
self.close(e);
e.stopImmediatePropagation();
break;
case $.ui.keyCode.UP:
case $.ui.keyCode.DOWN:
if (!self.menu.element.is(":visible")) {
e.stopImmediatePropagation();
}
}
});

// Check for the id_map as an attribute. This is for editing.

var id_map_string = this.element.attr('id_map');
if(id_map_string) this.id_map = jQuery.parseJSON(id_map_string);

this.ac = $.ui.autocomplete.prototype;
this.ac._create.apply(this, arguments);

this.updateHidden();

// Select function defined via options.
this.options.select = function(event, ui) {
var contents = self.contents;
var cursorPos = self.cursorPos;

// Save everything following the cursor (in case they went back to add a mention)
// Separate everything before the cursor
// Remove the trigger and search
// Rebuild: start + result + end

var end = contents.substring(cursorPos, contents.length);
var start = contents.substring(0, cursorPos);
start = start.substring(0, start.lastIndexOf(self.options.trigger));

var top = self.element.scrollTop();
this.value = start + self.options.trigger+ui.item.label+' ' + end;
self.element.scrollTop(top);

// Create an id map so we can create a hidden version of this string with id's instead of labels.

self.id_map[ui.item.label] = ui.item.value;
self.updateHidden();

/** Places the caret right after the inserted item. */
var index = start.length + self.options.trigger.length + ui.item.label.length + 2;
if (this.createTextRange) {
var range = this.createTextRange();
range.move('character', index);
range.select();
} else if (this.setSelectionRange) {
this.setSelectionRange(index, index);
}

return false;
};

// Don't change the input as you browse the results.
this.options.focus = function(event, ui) { return false; }
this.menu.options.blur = function(event, ui) { return false; }

// Any changes made need to update the hidden field.
this.element.focus(function() { self.updateHidden(); });
this.element.change(function() { self.updateHidden(); });
},

// If there is an 'img' then show it beside the label.

_renderItem: function( ul, item ) {
if(item.img != undefined) {
return $( "<li></li>" )
.data( "item.autocomplete", item )
.append( "<a>" + "<img src='" + item.img + "' /><span>"+item.label+"</span></a>" )
.appendTo( ul );
}
else {
return $( "<li></li>" )
.data( "item.autocomplete", item )
.append( $( "<a></a>" ).text( item.label ) )
.appendTo( ul );
}
},

// This stops the input box from being cleared when traversing the menu.

_move: function( direction, event ) {
if ( !this.menu.element.is(":visible") ) {
this.search( null, event );
return;
}
if ( this.menu.first() && /^previous/.test(direction) ||
this.menu.last() && /^next/.test(direction) ) {
this.menu.deactivate();
return;
}
this.menu[ direction ]( event );
},

search: function(value, event) {

var contents = this.element.val();
var cursorPos = this.getCursor();
this.contents = contents;
this.cursorPos = cursorPos;

// Include the character before the trigger and check that the trigger is not in the middle of a word
// This avoids trying to match in the middle of email addresses when '@' is used as the trigger

var check_contents = contents.substring(contents.lastIndexOf(this.options.trigger) - 1, cursorPos);
var regex = new RegExp('\\B\\'+this.options.trigger+'([\\w\\-]+)');

if (contents.indexOf(this.options.trigger) >= 0 && check_contents.match(regex)) {

// Get the characters following the trigger and before the cursor position.
// Get the contents up to the cursortPos first then get the lastIndexOf the trigger to find the search term.

contents = contents.substring(0, cursorPos);
var term = contents.substring(contents.lastIndexOf(this.options.trigger) + 1, contents.length);

// Only query the server if we have a term and we haven't received a null response.
// First check the current query to see if it already returned a null response.

if(this.stopIndex == contents.lastIndexOf(this.options.trigger) && term.length > this.stopLength) { term = ''; }

if(term.length > 0) {
// Updates the hidden field to check if a name was removed so that we can put them back in the list.
this.updateHidden();
return this._search(term);
}
else this.close();
}
},

// Slightly altered the default ajax call to stop querying after the search produced no results.
// This is to prevent unnecessary querying.

_initSource: function() {
var self = this, array, url;
if ( $.isArray(this.options.source) ) {
array = this.options.source;
this.source = function( request, response ) {
response( $.ui.autocomplete.filter(array, request.term) );
};
} else if ( typeof this.options.source === "string" ) {
url = this.options.source;
this.source = function( request, response ) {
if ( self.xhr ) {
self.xhr.abort();
}
self.xhr = $.ajax({
url: url,
data: request,
dataType: 'json',
success: function(data) {
if(data != null) {
response($.map(data, function(item) {
if (typeof item === "string") {
label = item;
}
else {
label = item.label;
}
// If the item has already been selected don't re-include it.
if(!self.id_map[label] || self.options.allowDuplicates) {
return item
}
}));
self.stopLength = -1;
self.stopIndex = -1;
}
else {
// No results, record length of string and stop querying unless the length decreases
self.stopLength = request.term.length;
self.stopIndex = self.contents.lastIndexOf(self.options.trigger);
self.close();
}
}
});
};
} else {
this.source = this.options.source;
}
},

destroy: function() {
$.Widget.prototype.destroy.call(this);
},

// Gets the position of the cursor in the input box.

getCursor: function() {
var i = this.element[0];

if(i.selectionStart) {
return i.selectionStart;
}
else if(i.ownerDocument.selection) {
var range = i.ownerDocument.selection.createRange();
if(!range) return 0;
var textrange = i.createTextRange();
var textrange2 = textrange.duplicate();

textrange.moveToBookmark(range.getBookmark());
textrange2.setEndPoint('EndToStart', textrange);
return textrange2.text.length;
}
},

// Populates the hidden field with the contents of the entry box but with
// ID's instead of usernames. Better for storage.

updateHidden: function() {
var trigger = this.options.trigger;
var top = this.element.scrollTop();
var contents = this.element.val();
for(var key in this.id_map) {
var find = trigger+key;
find = find.replace(/[^a-zA-Z 0-9@]+/g,'\\$&');
var regex = new RegExp(find, "g");
var old_contents = contents;
contents = contents.replace(regex, trigger+'['+this.id_map[key]+']');
if(old_contents == contents) delete this.id_map[key];
}
$(this.options.hidden).val(contents);
this.element.scrollTop(top);
}

}));
})( jQuery, window , document );

Comments

Sign in to comment.
despotbg   -  May 08, 2013
if you wont to search just by one word change those line
if (contents.indexOf(this.options.trigger) >= 0 && check_contents.match(regex)) ==>
if (contents.indexOf(this.options.trigger) >= 0 && check_contents.match(regex) && check_contents.indexOf(' ') <= 0)
 Respond  
despotbg   -  May 08, 2013
If you wont to search non latinic word change this line:
var regex = new RegExp('\\B\\'+this.options.trigger+'([\\w\\-]+)'); =>> var regex = new RegExp('\\B\\'+this.options.trigger+'([\\S\\-]+)');
 Respond  
despotbg   -  May 08, 2013
How to disable searching by multi words? I need just search by one word?
Hawkee  -  May 08, 2013
This is not something that can be disabled, but if your search returns no results after the user presses a space it will stop querying.
Sign in to comment

Piyush   -  Mar 21, 2013
After wasting a day on buggy jquery -input-mentions i found this working perfectly. Modified it for multi-trigger for my need. Here is quick hack with least possible change in existing code as others are also interested in it.
Add one more option at line 20
triggerArray: new Array('@','#'),

Add these in search function after assignment of variable at line 144 above

var triggerIndex = $.map(this.options.triggerArray,function(val,i){
return contents.lastIndexOf(val);
});
this.options.trigger = this.options.triggerArray[triggerIndex.indexOf(Math.max.apply(window,triggerIndex))];

Now request will be initiated at each element in triggerArray.
Hawkee  -  Mar 21, 2013
Great addition. How do you configure each dataset for each trigger?
Piyush  -  Mar 25, 2013
I think that won't be much of a problem. Passing an array of source on key value of trigger will do. Eg source: array('#'=>'/getHashTags' , '@'=>'getUserNames'). Then selecting the appropriate source in _initsource function.
Piyush  -  Mar 25, 2013
Hey i am facing an error when trying your this plugin with latest jquery ui and css bundle v 1.10.2. When traversing the items on autocomplete, error is throw at this line
if ( this.menu.first() && /^previous/.test(direction) [line 127]
Uncaught TypeError: Object [object Object] has no method 'first'
Any fix for that ?
Hawkee  -  Mar 25, 2013
This hasn't been tested with jQuery UI 1.9 or 1.10 so there will likely be errors. It's currently only supported on 1.8.
Piyush  -  Mar 25, 2013
Perhaps that is because of use of some jquery internal function. You should probably consider it remaking it w/o touching internal functions. Anyways after commenting this section of code
if ( this.menu.first() && /^previous/.test(direction) ||
this.menu.last() && /^next/.test(direction) ) {
this.menu.deactivate();
return;
}

everything seems to work fine. What exactly this part is doing ?
Currently i am adding one more feature to this plugin. Will update it once i am finished with it.
despotbg  -  May 07, 2013
Is there any chance to get multi-trigger soon? I need to have each dataset for each trigger :(
cgallegu  -  27 days ago
Add a "sources" property to the options in the following format: {"#" : 'url1', "@": 'url2'}.
Then modify your search function as following

var contents = this.element.val();
var cursorPos = this.getCursor();
var triggerIndex = $.map(this.options.triggerArray,function(val,i){
return contents.lastIndexOf(val);
});
this.options.trigger = this.options.triggerArray[triggerIndex.indexOf(Math.max.apply(window,triggerIndex))];
//Added for multi trigger datasource
if (this.options.sources !== undefined) {
this.options.source = this.options.sources[this.options.trigger];
this._initSource();
}
Sign in to comment

Bicks   -  Mar 19, 2013
Everything works well, just wanted to know if we can remove the trigger after the autosuggessions has been selected like in facebook or twitter..
 Respond  
sud_mrjn   -  Feb 14, 2013
How does search.php works for this .. Can I have whole project demo as zip.. Thanks!!
Hawkee  -  Feb 14, 2013
There is a link to the GitHub project, but that only contains this core jQuery widget. You'll need to implement your own search.php or you can look at my node.js server side example.
sud_mrjn  -  Feb 15, 2013
My search.php is like this
<?php

//connection information
$host = "localhost";
$user = "root";
$password = "";
$database = "mention_list";
$param = $_GET["term"];

//make connection
$server = mysql_connect($host, $user, $password);
$connection = mysql_select_db($database, $server);

//query the database
$query = mysql_query("SELECT * FROM friends WHERE name REGEXP '^$param'");

//build array of results
for ($x = 0, $numrows = mysql_num_rows($query); $x < $numrows; $x++) {
$row = mysql_fetch_assoc($query);

$friends[$x] = array("name" => $row["name"]);
}

//echo JSON to page

echo json_encode($friends);
mysql_close($server);

?>

But your widget also include trigger with search word eg. @cobol
Hawkee  -  Feb 17, 2013
Make sure you set the value and label fields before you do json_encode. You aren't creating your associative array correctly. It should be something like this:

Code

 
Sign in to comment

cmer   -  Dec 29, 2012
Great work!

It does 95% of what I need, however. Does anybody know an alternative that acts more like Facebook, that is, doesn't require a trigger character (uses capital letters), allows spaces in the name (ie: first and last name) and highlights mentions directly in the textarea?

I've been using jquery.mentionsInput but it requires a trigger and doesn't support multi-word names.

Thanks!
Hawkee  -  Jan 03, 2013
This does allow spaces, but you would have to modify the code to accept capital letter triggers. This currently doesn't support contenteditable div's, so you won't get the highlighting. jQuery UI 1.9 now supports autocomplete on contenteditable divs, so it is possible but this will need to be adapted to work with version 1.9.
Sign in to comment

Chalien   -  Dec 29, 2012
@Hawkee WORKS PERFECT! Thanks man
 Respond  
Hawkee   -  Dec 29, 2012
@Chalien I believe you would have to change lines 145 and 146. You'll have to remove the character check from the regular expression and consider the parts that count the number of characters after the trigger.
 Respond  
Chalien   -  Dec 29, 2012
@Hawkee Yeah and in that way works perfect. but right now i need to used just with the @ and show all the user in the app. can you give me some advice about this?
 Respond  
Hawkee   -  Dec 28, 2012
@Chalien You need at least one letter otherwise there wouldn't be anything to search against.
 Respond  
Chalien   -  Dec 28, 2012
@Hawkee great plugin! I have a question. can i trigger the autocomplete just typing the @ or always i need to type a letter character after that. trying changing the this.triggerRegexp but nothing happened. THANKS!
 Respond  
tusharvikky   -  Dec 24, 2012
@Hawkee can we know how we can link @username to his/her profile?
 Respond  
Hawkee   -  Dec 23, 2012
@imin My only guess is maybe at the time that you're initializing the plugin the position of the inbox is not known to JavaScript. I'd try to initialize it at a different point in time to see if that is the case.
 Respond  
imin   -  Dec 23, 2012
@Hawkee doesn't work.. most probably because the input field that I want to bind triggeredAutocomplete to is dynamic.. that's why I tried using .on earlier... hmmm
 Respond  
Hawkee   -  Dec 22, 2012
@imin Use $(document).ready(function() { $("#description").triggeredAutocomplete... });
 Respond  
imin   -  Dec 22, 2012
@Hawkee i tried using on("load" but nothing came out after i input @.. seems like triggeredAutocomplete is not binded this way
 Respond  
Hawkee   -  Dec 22, 2012
@imin Why do you apply the widget to the textarea upon clicking? Why not just apply it upon displaying it?
 Respond  
imin   -  Dec 22, 2012
nevermind i'm still awake.. here's my jsfiddle http://jsfiddle.net/imin/3HeZQ/ but it doesn't run there since jsfiddle doesn't support load php page using ajax right? but maybe you can get the idea on what i did there.. thanks!
 Respond  
imin   -  Dec 22, 2012
@hawkee just to add my text field is not only dynamically loaded, it's also inside a table (the table is dynamically loaded)
 Respond  
imin   -  Dec 22, 2012
@hawkee hmm not yet but I'm going to create it later. getting sleepy now (it's nearly 2am here)
 Respond  
Hawkee   -  Dec 22, 2012
@imin Do you have a jsfiddle I can look at?
 Respond  
imin   -  Dec 22, 2012
i use .on to bind this triggeredAutocomplete to a dynamically loaded text field which is located somewhere on the center of the screen; but instead of the autocomplete suggestion list being displayed under/nearby the dynamically loaded text field, it is being displayed on the top left corner of the screen. any idea where should I alter to fix this? thanks!
 Respond  
brunosm   -  Dec 15, 2012
@Hawkee Excellent your plugins, recently I'm starting in this world and I have a question, as I can do to take the data from a mysql database? . Thank you!
 Respond  
Hawkee   -  Dec 09, 2012
@Beakid That may work if you remove "+ self.options.trigger" from line 73 and you edit updateHidden to search for the usernames without the trigger. It may require some testing and experimenting, but it should be doable.
 Respond  
Beakid   -  Dec 09, 2012
Great script! Is there a way to remove the @-sign when you have selected something from the list? Would be awesome.
 Respond  
Hawkee   -  Dec 05, 2012
@patojutard Interesting solution. I like the elegance of the code, but it is lacking in some functionality. I don't see a way to query against a server for the data.
 Respond  
patojutard   -  Dec 05, 2012
You can try this jquery plug-in that allows in-line autocomplete: https://github.com/tactivos/jquery-sew
 Respond  
victoracioli   -  Dec 04, 2012
@Hawkee thank you! I resolved the problem! It's really a problem how I was presenting the id_map .
I passed to id_map an array like this "[{"nick":"id"},{"nick2":"id2"}]" and the correct format (json format) is "{"nick":"id","nick2":"id2"}", and finally it's working correctly.

Thank you for your help!
 Respond  
Hawkee   -  Dec 04, 2012
@victoracioli Do you have an example online? It could be a problem with how your presenting your id_map when you build the edit form.
 Respond  
Are you sure you want to unfollow this person?
Are you sure you want to delete this?
Click "Unsubscribe" to stop receiving notices pertaining to this post.
Click "Subscribe" to resume notices pertaining to this post.