In one of the projects I’m working on for Montrose River, I’ve needed to write a Facebook-like tagging system – when writing a note about a student, it allows you to choose another student to link to. The Facebook “What’s On Your Mind?” box allows you to press the @ key and then choose from a list of your friends. When you post the update to your news feed, it then links to that person’s profile.


I couldn’t find any code to do this, so I wrote my own version.
I tried to approach this using a <textarea> to begin with, but quickly realised that you can’t put HTML code inside them! To make this work, it requires using a property on a <div> – contentEditable.
The Form
<div contentEditable="true"></div>
<ul id="students_list"></ul>
CSS
Just a bit of styling to make the dropdown options look nice…
div.textarea {
height: 50px;
margin-bottom: 15px;
}
ul#students_list {
background: #F9F9F9;
padding: 10px;
border: 1px solid #BFBFBF;
border-top: none;
display: none;
}
ul#students_list li {
font-size: 12px;
margin-bottom: 0;
}
ul#students_list li:hover {
cursor: pointer;
}
The Backend
My backend code simply returns a JSON array of any students matching the search result when I visit /students/.json.
/students/search/Fr.json
[{"student":
{"id":1,
"name":"Fred Bloggs"}
}]
The Javascript
$('div.textarea').live('keypress', function(e) {
// Hide the dropdown
$('ul#students_list').hide();
// If we press the @ key or have started tracking
// e.keyCode works in everything but Firefox which needs e.which
keyCode = (e.keyCode ? e.keyCode : e.which);
if (keyCode == 64 || tracking == true) {
tracking = true;
// Replace HTML-encoded spaces with a standard space character
str = $(this).html().replace( ' ', '%20' );
// Get the string from the last @ character to end of the string
searchCaret = str.lastIndexOf('@');
searchString = str.substring( searchCaret ).replace( '@', '' );
$.getJSON('/students/search/' + searchString + '.json', function(data) {
if( data.length > 0 ) {
for( i = 0; i < data.length; i++ ) {
$('ul#students_list').html( '<li>' + data[i].student.name + '</li>' );
}
$('ul#students_list').show();
}
});
}
});
$('ul#students_list li').live( 'click', function(e) {
// Get the contents of the string before the @
newContent = str.substring( 0, searchCaret );
// Add the name of the student
// Intentionally blank, add your own link/formatting in here
newContent += '<a>' + $(this).html() + '</a>';
$('div.textarea').html( newContent.replace( '%20', ' ' ) );
$('div.textarea').focus();
$('ul#students_list').hide();
});
Essentially, the JavaScript code does the following:
- If the @ key has been pressed, start “tracking” (send requests to the server from now on).
- If tracking is enabled, find the last @ in the string and get the content between the @ and the end of the string (so “This is a post about @Fr” returns “Fr”).
- If the JSON query returns results, populate the
<ul> with list items and make it visible.
- When a
<li> is clicked, replace the @ to the end of the string with the content of the <li> (so “@Fr” is replaced with “Fred”).
The Result


Notes
I’ve tested this in Firefox (3.6+, I think contentEditable wasn’t introduced before then so there may have to be some looking into alternative solutions), IE6-8, Safari, and Chrome, and it seems to work fine. I’d have liked to have included being able to use the arrow keys to page up and down the list, and enter to select, and this might be something I’ll add in the future. I’m hoping if the jQuery UI Menu project makes it to stable, this will be very easy to integrate.
Calling .focus() on the <div> seems to have different results in different browsers – ideally this would go to the end of the content in the <div>. If anyone has a solution, please let me know in the comments!
Comments, as ever, are appreciated.