58

I am looking for a Javascript autocomplete implementation which includes the following:

  • Can be used in a HTML textarea
  • Allows for typing regular text without invoking autocomplete
  • Detects the @ character and starts autocomplete when it is typed
  • Loads list of options through AJAX

I believe that this is similar to what Twitter is doing when tagging in a tweet, but I can't find a nice, reusable implementation.
A solution with jQuery would be perfect.

Thanks.

Martin Wiboe
  • 2,119
  • 2
  • 28
  • 50

11 Answers11

28

Another great library which solves this problem At.js (deprecated)

Source

Demo

They are now suggesting the Tribute library

https://github.com/zurb/tribute

Example

Jernej Novak
  • 3,165
  • 1
  • 33
  • 43
  • That lib works best with contentEditable which is not as cross-browser as textarea. If it's desired to simulate highlights / icons *with/on textarea* there are better options. – valk May 14 '18 at 07:07
  • 1
    I was facing issues with Ckeditor, At.js solves the issues thanks! – shery089 Sep 22 '20 at 12:36
21

I'm sure your problem is long since solved, but jquery-textcomplete looks like it would do the job.

Kevin Schaper
  • 261
  • 2
  • 3
  • Can be difficult to handle mentions with this one, such as tracking which users have been mentioned, which have been removed, depending on how you represent mentioned users. – alex Sep 02 '15 at 07:40
9

Have you tried this

GITHUB: https://github.com/podio/jquery-mentions-input

DEMO/CONFIG: http://podio.github.io/jquery-mentions-input/

It is pretty simple to implement.

alex
  • 479,566
  • 201
  • 878
  • 984
Andrej Kaurin
  • 11,592
  • 13
  • 46
  • 54
8

I've created a Meteor package for this purpose. Meteor's data model allows for fast multi-rule searching with custom rendered lists. If you're not using Meteor for your web app, (I believe) you unfortunately won't find anything this awesome for autocompletion.

Autocompleting users with @, where online users are shown in green:

enter image description here

In the same line, autocompleting something else with metadata and bootstrap icons:

enter image description here

Fork, pull, and improve:

https://github.com/mizzao/meteor-autocomplete

Andrew Mao
  • 35,740
  • 23
  • 143
  • 224
  • Saw you present this at a Meteor Dev Shop awhile back--october maybe? Great work my man – Brian Apr 20 '15 at 15:44
7

Try this:

(function($){
    
        $.widget("ui.tagging", {
            // default options
            options: {
                source: [],
                maxItemDisplay: 3,
                autosize: true,
                animateResize: false,
                animateDuration: 50
            },
            _create: function() {
                var self = this;
                
                this.activeSearch = false;
                this.searchTerm = "";
                this.beginFrom = 0;
    
                this.wrapper = $("<div>")
                    .addClass("ui-tagging-wrap");
                
                this.highlight = $("<div></div>");
                
                this.highlightWrapper = $("<span></span>")
                    .addClass("ui-corner-all");
    
                this.highlightContainer = $("<div>")
                    .addClass("ui-tagging-highlight")
                    .append(this.highlight);
    
                this.meta = $("<input>")
                    .attr("type", "hidden")
                    .addClass("ui-tagging-meta");
    
                this.container = $("<div></div>")
                    .width(this.element.width())
                    .insertBefore(this.element)
                    .addClass("ui-tagging")
                    .append(
                        this.highlightContainer, 
                        this.element.wrap(this.wrapper).parent(), 
                        this.meta
                    );
                
                var initialHeight = this.element.height();
                
                this.element.height(this.element.css('lineHeight'));
                
                this.element.keypress(function(e) {
                    // activate on @
                    if (e.which == 64 && !self.activeSearch) {
                        self.activeSearch = true;
                        self.beginFrom = e.target.selectionStart + 1;
                    }
                    // deactivate on space
                    if (e.which == 32 && self.activeSearch) {
                        self.activeSearch = false;
                    }
                }).bind("expand keyup keydown change", function(e) {
                    var cur = self.highlight.find("span"),
                        val = self.element.val(),
                        prevHeight = self.element.height(),
                        rowHeight = self.element.css('lineHeight'),
                        newHeight = 0;
                    cur.each(function(i) {
                        var s = $(this);
                        val = val.replace(s.text(), $("<div>").append(s).html());
                    });
                    self.highlight.html(val);
                    newHeight = self.element.height(rowHeight)[0].scrollHeight;
                    self.element.height(prevHeight);
                    if (newHeight < initialHeight) {
                        newHeight = initialHeight;
                    }
                    if (!$.browser.mozilla) {
                        if (self.element.css('paddingBottom') || self.element.css('paddingTop')) {
                            var padInt =
                                parseInt(self.element.css('paddingBottom').replace('px', '')) + 
                                parseInt(self.element.css('paddingTop').replace('px', ''));
                            newHeight -= padInt;
                        }
                    }
                    self.options.animateResize ?
                        self.element.stop(true, true).animate({
                                height: newHeight
                            }, self.options.animateDuration) : 
                        self.element.height(newHeight);
                    
                    var widget = self.element.autocomplete("widget");
                        widget.position({
                            my: "left top",
                            at: "left bottom",
                            of: self.container
                        }).width(self.container.width()-4);
                    
                }).autocomplete({
                    minLength: 0,
                    delay: 0,
                    maxDisplay: this.options.maxItemDisplay,
                    open: function(event, ui) {
                        var widget = $(this).autocomplete("widget");
                        widget.position({
                            my: "left top",
                            at: "left bottom",
                            of: self.container
                        }).width(self.container.width()-4);
                    },
                    source: function(request, response) {
                        if (self.activeSearch) {
                            self.searchTerm = request.term.substring(self.beginFrom); 
                            if (request.term.substring(self.beginFrom - 1, self.beginFrom) != "@") {
                                self.activeSearch = false;
                                self.beginFrom = 0;
                                self.searchTerm = "";
                            }
                            if (self.searchTerm != "") {
                                
                                if ($.type(self.options.source) == "function") {
                                    self.options.source(request, response);                   
                                } else {
                                    var re = new RegExp("^" + escape(self.searchTerm) + ".+", "i");
                                    var matches = [];
                                    $.each(self.options.source, function() {
                                        if (this.label.match(re)) {
                                            matches.push(this);
                                        }
                                    });
                                    response(matches);
                                }
                            }
                        }
                    },
                    focus: function() {
                        // prevent value inserted on focus
                        return false;
                    },
                    select: function(event, ui) {
                        self.activeSearch = false;
                        //console.log("@"+searchTerm, ui.item.label);
                        this.value = this.value.replace("@" + self.searchTerm, ui.item.label) + ' ';
                        self.highlight.html(
                            self.highlight.html()
                                .replace("@" + self.searchTerm,
                                         $("<div>").append(
                                             self.highlightWrapper
                                                 .text(ui.item.label)
                                                 .clone()
                                         ).html()+' ')
                        );
                            
                        self.meta.val((self.meta.val() + " @[" + ui.item.value + ":]").trim());
                        return false;
                    }
                });
    
            }
        });
body, html {
        font-family: "lucida grande",tahoma,verdana,arial,sans-serif;
    }
    
    .ui-tagging {
        position: relative;
        border: 1px solid #B4BBCD;
        height: auto;
    }
    
    .ui-tagging .ui-tagging-highlight {
        position: absolute;
        padding: 5px;
        overflow: hidden;
    }
    .ui-tagging .ui-tagging-highlight div {
        color: transparent;
        font-size: 13px;
        line-height: 18px;
        white-space: pre-wrap;
    }
    
    .ui-tagging .ui-tagging-wrap {
        position: relative;
        padding: 5px;
        overflow: hidden;
        zoom: 1;
        border: 0;
    }
    
    .ui-tagging div > span {
        background-color: #D8DFEA;
        font-weight: normal !important;
    }
    
    .ui-tagging textarea {
        display: block;
        font-family: "lucida grande",tahoma,verdana,arial,sans-serif;
        background: transparent;
        border-width: 0;
        font-size: 13px;
        height: 18px;
        outline: none;
        resize: none;
        vertical-align: top;
        width: 100%;
        line-height: 18px;
        overflow: hidden;
    }
    
    .ui-autocomplete {
        font-size: 13px;
        background-color: white;
        border: 1px solid black;
        margin-bottom: -5px;
        width: 0;
    }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<textarea></textarea>

http://jsfiddle.net/mekwall/mcWnL/52/ This link will help you

APerson
  • 8,140
  • 8
  • 35
  • 49
Duy NGuyen
  • 79
  • 3
  • 2
    Seems to work at first, but after I accept the first autocomplete and try to make a second, the highlighting quickly disappears on the second name. If I try to continue typing after that, the tab stops responding and eventually crashes. (This is in Chrome.) – mmitchell Apr 02 '13 at 17:54
6

I could not find any solution that matched my requirements perfectly, so I ended up with the following:

I use the jQuery keypress() event to check for the user pressing the @ character.
If this is the case, a modal dialog is shown using jQuery UI. This dialog contains an autocomplete text field (many options can be used here, but I recommmend jQuery Tokeninput)
When the user selects an option in the dialog, a tag is added to the text field and the dialog is closed.

This is not the most elegant solution, but it works and it does not require extra keypresses compared to my original design.

Edit
So basically, we have our large text box where the user can enter text. He should be able to "tag" a user (this just means inserting #<userid> in the text). I attach to the jQuery keyup event and detect the @ character using (e.which == 64) to show a modal with a text field for selecting the users to tag.

The meat of the solution is simply this modal dialog with a jQuery Tokeninput text box. As the user types here, the list of users is loaded through AJAX. See the examples on the website for how to use it properly. When the user closes the dialog, I insert the selected IDs into the large text box.

Martin Wiboe
  • 2,119
  • 2
  • 28
  • 50
4

Recently i had to face this problem and this is how i nailed down...

  1. Get the string index at the cursor position in the textarea by using selectionStart
  2. slice the string from index 0 to the cursor position
  3. Insert it into a span (since span has multiple border boxes)
  4. Get the dimensions of the border box using element.getClientRects() relative to the view port. (here is the MDN Reference)
  5. Calculate the top and left and feed it to the dropdown

This works in all latest browsers. haven't tested at old ones

Here is Working bin

selvagsz
  • 3,852
  • 1
  • 24
  • 34
1

Another plugin which provides similar functionality:

AutoSuggest

You can use it with custom triggers or you can use it without any triggers. Works with input fields, textareas and contenteditables. And jQuery is not a dependency.

AvcS
  • 2,263
  • 11
  • 18
1

I would recommend the textcomplete plugin. No jQuery dependency. You may need bootstrap.css to refer, but I recommend to write your own CSS, lighter and simple.

Follow the below steps to give it a try

  1. npm install @textcomplete/core @textcomplete/textarea
  2. Bind it to your input element
    const editor = new TextareaEditor(inputEl);
    const textcomplete = new Textcomplete(editor, strategy, options);
    
  3. Set strategy(how to fetch suggestion list) and options(settings to configure the suggestions) according to your need.

enter image description here

JS version

Angular Version

Pankaj Parkar
  • 134,766
  • 23
  • 234
  • 299
0

This small extension seems to be the closest at least in presentation to what was asked. Since it's small, it can be easily understood and modified. http://lookapanda.github.io/jquery-hashtags/

valk
  • 9,363
  • 12
  • 59
  • 79
-3

THIS should work. With regards to the @ kicking off the search, just add (dynamically or not) the symbol to the beginning of each possible search term.

Community
  • 1
  • 1
Ashley Staggs
  • 1,557
  • 9
  • 24
  • 38
  • 1
    This won't work, because it assumes that the entire text is tokenized. That is, if you write some text and then a @, it will not kick off since it reads from the start of the line. – Martin Wiboe Jun 04 '11 at 11:48