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 );
Great plugin! I was wondering if there's a way to display the label instead of the value in the text input? In jQuery autocomplete you can add option:
select: function (event, ui) {
event.preventDefault();
$(this).val(ui.item.label);
}
Is there something like the above supported in this plugin? @Hawkee
GREAT !
Thanks @Hawkee !
I was trying to find exactly this autocomplete plugin and I was fighting with a lot of others plugin. Most of the time they don't work or they are nightmares to configure. And when they are OK, they don't work on a textarea displayed on a jquery dialog box :-(
Yours is perfect, light, very easy to use and working into a dialog box !
The only bug was the drop-down list can't be used with the up and down arrows but the hack of @UIdezigner solved that.(I'm using Jquery UI 1.11)
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!
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
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
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?
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
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 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.
@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.
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.
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 ?
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.
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();
}
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.
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.
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
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!
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.
That would require a fairly extensive rewrite. You may want to check into alternatives that offer that sort of functionality or you are more than welcome to add that functionality and do a pull request.