Esteban Garcia Esteban Garcia - 1 year ago 77
Javascript Question

Problems making a autocomplete-mention system likes facebook's

im making a mention system like the one in facebook, right now I achieved it with a textarea, but now I want it to work with a contenteditable div so I can format the text within.

So right now what is does is when you press the "@" and a letter it searchs in a array the results containing that letter using the JQuery-UI autocomplete, when you select the result it displays the result in the div, but now i want to highlight the mention like facebook, so I put a:

<b style="background: #somecolor"></b>

So the mention was looking like this:

<b style="background: #somecolor">Mention</b>

So far so good, but when I want to keep writing, all the new text I wrote was going inside the
looking like this:

<b style="background: #somecolor">Mention all my new text</b>

Instead of
<b style="background: #somecolor">Mention</b> all my new text

So all the new text was being highlighted, here is a picture:

So this is the javascript code im using

$("#Conversacion").bind("keydown", function(event) {
if (event.keyCode === $.ui.keyCode.TAB && $(this).data("autocomplete") {

minLength: 0,
source: function(request, response) {
var term = request.term,
results = [];
if (term.indexOf("@") >= 0) {
term = extractLast(request.term);
if (term.length > 0) {
results = $.ui.autocomplete.filter(availableTagsFollowing, term);
} else {
//results = ['Start typing...'];
focus: function() {
// prevent value inserted on focus
return false;
select: function(event, ui) {
var terms = split(this.innerHTML);
// remove the current input
// add the selected item
terms.push('<b style="background:#E0FFC5">' + ui.item.label + '</b>');
this.innerHTML = terms.join("");

$('#mentionsHidden').val($('#mentionsHidden').val() + '@['+ui.item.value+':'+ui.item.label+']');
mentionsString = $('#mentionsHidden').val();
return false;


And this is the HTML Code

<div name="Descripcion" id="Conversacion" class="Conversacion" contenteditable="true"></div>

This is a picture of the Chrome's Inspector:

I looked in facebook to see how it's done, this is a picture of the chrome's inspector in facebook:

3.png (Same domain and folder of the other two images, sorry about this, this is a new user and I can't post more than 2 links)

It's seems that facebook when you select a person to mention creates a new line into the span that it uses so the text is separated from the

Well i hope that someone can help me.

By the way, I'm terrible sorry for my english, I'm from south america.

Answer Source

I'm just paraphrasing / summarizing the your idea: Your div is editable and the user types into it. At some condition you're presenting a list of which the user can select. Once the selection is made, the div's content is modified to contain the selected value within some wrapper element. Afterwards the user types on but the characters are inserted into the wrapper instead of after the wrapper, right?

The reason for the unwanted behavior is the focus position. Technically speaking, there is a range of selected content that gets replaced by the user's typed characters. This range is often invisible because it spans over exactly 0 characters. It spans over some content (and possibly multiple elements) when the mouse is used to select text for example.

Now let us assume the following HTML: <b>One</b><b>Two</>. It might be that the empty range is in between the b tags. When the next character A is entered it needs to be inserted into the document – but even though the range/caret/cursor position is known there are 3 possible destinations:

  • At the end of the first b: <b>OneA</b><b>Two</b>
  • In between the tags: <b>One</b>A<b>Two</b>
  • At the beginning of the second b: <b>One</b><b>ATwo</b>

Your browser chooses the first approach: Roughly speaking, to keep adding to the most recently added element.

Now, there is a pretty complex range system to specify which approach to use for placing new content. See Introduction to Range and a question on setting the range for examples.

The general idea to solve your problem is to create some empty content after the newly inserted content. Thus ending up with <b style="background: #somecolor">Mention</b>|. Note that instead of the pipe character | you need to place an empty text node but that can't be shown in HTML code. DOM is able to handle more than serialized HTML / HTML code! Then you would need to select the contents of the empty text node. The next character would then replace the empty text node (the range selecting the empty content within the text node would be replaced and thus the character would be inserted into the text node to be more precise). The effect is you end up with <b style="background: #somecolor">Mention</b>A assuming the entered character was A.

In order to implement the idea I suggest to:

  • Avoid using innerHTML and use DOM methods instead. innerHTML cannot create empty text nodes.
  • Read the documentation on Document Object Model Range carefully.

I prepared a jsFiddle to show a basic implementation. Note that this is a quick hack and supports neither ranges spanning multiple elements nor empty parents. The example wraps all upper case letters in a b tag which should be enough to get you started.

// Get the current selection and range
var selection = window.getSelection();
var range = selection.getRangeAt(0);

var textNode = range.startContainer;    
var text =;
// Create a new node to hold the text in front of the range
var textBeforeAddedContent = document.createTextNode(text.substr(0, range.startOffset));
// Update current node to remove everything which goes into the just create node and the content to be replaced = text.substr(range.endOffset);
// Put stuff in front of the range into the document. Unless a non-empty selection was made the content looks now exactly as before calling this code
textNode.parentNode.insertBefore(textBeforeAddedContent, textNode);

// Place the new content in between the text nodes
var wrapper = document.createElement('b');
textNode.parentNode.insertBefore(wrapper, textNode);

// You might want to set the new range explicitly here and add an empty text node wherever newly typed letters shall go
// Set the new selection explicitly
range.setStart(textNode, 0);
range.setEnd(textNode, 0);

Chrome fails for zero-length selections starting at offset 0 and sets a zero-length selection to end of previous child instead. An ugly hack to support Chrome could be to insert some invisible character and remove it after the next character was inserted.

Chrome's problems should not matter too much for the original question because it could be valid to insert a space after the wrapper element. The range's starting offset would then be set to 1 to insert new content after the space.

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download