文本框中的Twitter风格自动完成

58
我正在寻找一个JavaScript自动完成实现,它包括以下功能:
  • 可以在HTML文本区域中使用
  • 允许输入普通文本而不触发自动完成
  • 检测到字符@时,输入后开始自动完成
  • 通过AJAX加载选项列表
我认为这与Twitter在推文中标记时所做的类似,但我找不到一个好的、可重用的实现。
带有jQuery的解决方案将是完美的。
谢谢。

2
https://github.com/jakiestfu/Mention.js 可能是另一个选择。 - Kzqai
11个回答

28

该库最适合使用contentEditable,但它不像textarea那样兼容所有浏览器。如果希望在textarea上模拟高亮/图标,则有更好的选择。 - valk
1
我在使用Ckeditor时遇到了问题,但At.js解决了这些问题,谢谢! - shery089

21

我相信您的问题早已解决,但是jquery-textcomplete 看起来可以胜任这项工作。


使用此功能可能比较棘手,例如跟踪哪些用户已被提及,哪些已被移除,具体取决于您如何表示被提及的用户。 - alex

9

2
演示程序出现问题。下划线导入失败,返回404错误。 - Zoran Pavlovic

8
我已经为此创建了一个Meteor包。Meteor的数据模型允许使用自定义呈现的列表进行快速的多规则搜索。如果您没有在Web应用程序中使用Meteor,(我相信)您不会找到任何像这样令人惊叹的自动完成功能。
通过@自动完成用户,其中在线用户显示为绿色:

enter image description here

在同一行中,使用元数据和Bootstrap图标自动完成其他内容:

enter image description here

分叉,拉取和改进:

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


我在一段时间前的 Meteor Dev Shop 见过你演示这个项目,可能是在十月份吧?做得非常好,伙计。 - Brian

7

试试这个:

(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>

这个链接会对你有所帮助。


2
一开始似乎可以工作,但是在我接受第一个自动完成并尝试进行第二个时,第二个名称的高亮很快就消失了。如果我在那之后继续输入,选项卡会停止响应并最终崩溃。(这是在Chrome中发生的。) - mmitchell

6
我找不到完全符合我的要求的解决方案,所以我最终采用了以下方法:
我使用jQuery的 keypress() 事件来检查用户是否按下了 @ 字符。
如果是这种情况,就会使用jQuery UI显示一个模态对话框。此对话框包含自动完成文本字段(可以使用很多选项,但我推荐使用jQuery Tokeninput
当用户在对话框中选择选项时,将向文本字段添加标签并关闭对话框。
这不是最优雅的解决方案,但它能正常工作,并且与我的原始设计相比不需要额外的按键。 编辑
基本上,我们有一个大型文本框,用户可以在其中输入文本。他应该能够“标记”用户(这只是在文本中插入 #<userid>)。我附加到jQuery的 keyup 事件并使用 (e.which == 64) 来检测 @ 字符,从而显示具有选取用户文本字段的模态框。
解决方案的核心就是这个带有jQuery Tokeninput 文本框的模态对话框。当用户在此处键入时,通过 AJAX 加载用户列表。请参阅网站上的示例以正确使用它。当用户关闭对话框时,我将所选ID插入到大文本框中。

嘿,你有你的解决方案的链接或 jsfiddle 吗?我已经尝试了所有可用的库,但没有一个完全符合要求。 - wuliwong
我添加了进一步的解释 :) - Martin Wiboe

4

最近我遇到了这个问题,以下是我的解决方法...

  1. 使用selectionStart获取文本区域中光标位置的字符串索引
  2. 从索引0到光标位置切片字符串
  3. 将其插入到一个span中(因为span有多个边框框)
  4. 使用element.getClientRects()相对于视口获取边框框的尺寸(这里是MDN参考
  5. 计算顶部和左侧位置并将其提供给下拉菜单

这适用于所有最新的浏览器。未在旧版本中测试过。

这里是工作示例


1
我建议使用textcomplete插件。它不依赖jQuery。您可能需要引用bootstrap.css,但我建议编写自己的CSS,更轻量简单。
按照以下步骤尝试一下:
  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.

在此输入图像描述

JS版本

Angular版本


1

另一个提供类似功能的插件:

AutoSuggest

您可以使用自定义触发器,也可以不使用任何触发器。适用于输入字段、文本区域和可编辑内容。而且,它不依赖于jQuery。


0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接