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.
dannythebestguy   -  Jun 06, 2014
This does not work with TinyMCE. Is there a way around to get it working with TinyMCE
 Respond  
Maaruen   -  May 17, 2014
Hi, I am using this code in my app, and, then the second @, code don't work :(
In this case https://smartican.com/tagtest.php don't work neither.
any idea?
Thanks!
Hawkee  -  May 17, 2014
That certainly is strange. The versions match what I've got on jsfiddle, so my only thought is there is some encoding or whitespace issue. I noticed you're using a 1.7.2 theme rather than a 1.7.1 theme, but I don't think that should matter.
Sign in to comment

karo   -  May 10, 2014
great work!

please solve this bug and it will be even greater! ;)
it probably has to do with some regex somewhere, and it's not my strong side...
karo  -  May 10, 2014
hey there was supposed to be an image in my comment.
here it is, hosted: http://ovoono.com/temp/autocompletebug.jpg
it shows the "hidden" output, and the @[ id ] bug with @JavaScript, obviously because @Java is a substring of @JavaScript
Sign in to comment

charakalmast   -  Mar 03, 2014
Hey
How do i store a predefined json in a varaible as a source? I am getting a json in document.ready via ajax. I then store it in a variable and use it in the source, but this doesnt work.
charakalmast  -  Mar 03, 2014
and also how do i limit the number of results?
Hawkee  -  Mar 03, 2014
Make sure your json matches the format required for this to work. You can check the jsfiddle for a working example. As far as a limit, there is no functionality for that, but this might help: http://stackoverflow.com/questions/7617373/limit-results-in-jquery-ui-autocomplete
charakalmast  -  Mar 03, 2014
Hey
The link worked for me.. I also figured out that i had to convert the string into a json object using JSON.parse(string) to make the variable work.
Can you also tell me how i can change the thing we search for? i.e. this current searches the label data, can i change it to something else
Hawkee  -  Mar 03, 2014
Why change it to something else?
charakalmast  -  Mar 04, 2014
actually i had implemented the whole autocomplete and used the label as something. i had changed a lot of code. now i want to add another searching factor which i will concat with some other value in the json.
Hawkee  -  Mar 04, 2014
You would have to modify the code to support multiple triggers.
Sign in to comment

HolyG   -  Jan 07, 2014
Hi
I have this working with an external source using the default @ trigger how might i adapt it So the trigger is either a “@” or “#” and to be able to pass the trigger into the source script to perform two different queries
like
/source.pl?term=MyTerm&trigger=#
also is there a way to set minLength:3 and a delay?
Hawkee  -  Jan 07, 2014
This doesn't support two triggers, but you can change the default trigger in the options. Refer to the option descriptions at the top of the code.
HolyG  -  Jan 08, 2014
OK If i have this in my HTML header

Is this Trigger overriding the Trigger in the script options?

I How do I pass the '#' into the source
Either like
source: "/cgi-bin/AorHSearch1.pl?term#MyTerm",
or
source: "/cgi-bin/AorHSearch1.pl?term=MyTerm&trigger=#",

$(function() {
$('#inputbox').triggeredAutocomplete({
hidden: '#hidden_inputbox',
source: "/cgi-bin/AorHSearch.pl",
triger: "#"
});

Thank you
Hawkee  -  Jan 15, 2014
The trigger isn't passed along to the source. It just indicates that the text following it needs to be passed to the source.
Sign in to comment

charakalmast   -  Dec 03, 2013
How can i style the option list?
Hawkee  -  Dec 03, 2013
You'll have to use a different jQuery UI theme.
charakalmast  -  Dec 06, 2013
I tried a few different themes and found a good one, but i could not find what to manipulate in the css of the themes. I wanted to change the z-index of the option list, can you tell me what exactly i should change for that?
Hawkee  -  Dec 07, 2013
You probably want to inspect the element when it is open to find the class path. Then just add that to your custom CSS and adjust the z-index.
charakalmast  -  Dec 08, 2013
Hey. Thanks for the help. i got everything working fine now, but i have another question.
Is there a way i could change the color of the test that gets appended to the textarea?
The search also seems to be a bit slower than the ones i have used at other places. Any tips on how i can make it faster?
Hawkee  -  Dec 08, 2013
You'd have to adapt this to work for a contenteditable div to be able to change the colors. This would require some fairly complicated edits to the code. Are you using a server side search? The speed is dependent on your server. I like to use Sphinx for my server side searching as it's very fast.
charakalmast  -  Dec 09, 2013
thanks for all your help. will look into it.
Sign in to comment

charakalmast   -  Dec 03, 2013
Not working for me. I tried creating a simple version of this here: https://smartican.com/tagtest.php but it doesnt seem to work. What am i missing?
Hawkee  -  Dec 03, 2013
Try putting it into a $(document).ready function and see if that fixes it.
charakalmast  -  Dec 03, 2013
works now.. thanks!!
Sign in to comment

brewpoo   -  Oct 16, 2013
Is there anyway to position the drop down box in the general area of the cursor?
Hawkee  -  Oct 16, 2013
There was another project that branched from this on GitHub that does it. Follow the link above to the GitHub page.
Sign in to comment

sloankev   -  Sep 29, 2013
@Hawkee this is working great! Thank you! I'm dealing with a large userbase and was just wondering how I could add a minLength attribute to only show results after typing 2 or 3 letters after the trigger character or only a certain number of results at a time. Any help would be appreciated.
Hawkee  -  Sep 30, 2013
You probably want to do this server-side. You may need to edit this slightly so self.stopLength has a minimum value. Otherwise the null result will stop the queries. You'll want it to continue querying until your min length is reached.
Sign in to comment

tushar_vikky   -  Jul 22, 2013
auto @mention user just like twitter has on its reply box. Is it possible?
Hawkee  -  Jul 22, 2013
Yes, you just pre-populate the hidden field with the userid in the format above and pre-populate the textarea with the visual representation of the mention.
tushar_vikky  -  Jul 22, 2013
I did that. But as soon as I select the textbox to type, it updates and copies the text to hidden_input.
Hawkee  -  Jul 22, 2013
Ah yes, because the id_map is not defined. You'll also need to pre-populate that as well.
tushar_vikky  -  Jul 22, 2013
It would help alot if you can point me to an example. And also how to define the id_map.
Hawkee  -  Jul 22, 2013
@tushar_vikky It is mentioned above. You'll just need to create a json encoded array with a single element representing the user you'd like pre-populated. If you need to see it in action try editing a comment you made here with a @mention and view the source for the id_map.
tushar_vikky  -  Jul 22, 2013
@Hawkee thanx a lot. It works perfect. :)
arimk  -  Feb 20, 2014
@Hawkee just to test the id_map as I have trouble using it
edit: got it to work now :)
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  -  May 23, 2013
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();
}
UIdezigner  -  Jul 12, 2013
For the lines that Piyush commented out..... user should replaced with the following to work with jquery-ui 1.9.

if ( this.menu.isFirstItem() && /^previous/.test(direction) ||
this.menu.isLastItem() && /^next/.test(direction) ) {
this._value( this.term );
this.menu.blur();
return;
}

hooo wa !
PS.
this part allows navigation with arrow keys once menu in displayed.
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  
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.