制作像 Facebook 这样的自动完成提及系统时遇到的问题

发布于 2024-12-04 23:53:11 字数 2894 浏览 1 评论 0原文

我正在制作一个类似于 facebook 中的提及系统,现在我使用文本区域实现了它,但现在我希望它与 contenteditable div 一起使用,以便我可以格式化其中的文本。

现在,当您按下“@”和一个字母时,它会使用 JQuery-UI 自动完成功能在数组中搜索包含该字母的结果,当您选择结果时,它会在 div 中显示结果,但现在我想要像 facebook 一样突出显示提及,所以我输入了:

所以提及看起来像这样:

< ;b 样式=“背景: #somecolor">提及

到目前为止一切顺利,但是当我想继续写作时,我写的所有新文本都在< /code> 看起来像这样:

提及我所有的新文本

而不是 提及我所有的新文本

所以所有新文本都被突出显示,这是一张图片:

http ://dev.frinki.com/question/1.png

这是我使用的javascript代码

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

        }).autocomplete({
            minLength: 0,
            source: function(request, response) {
                var term = request.term,
                    results = [];
                    console.log(term);
                if (term.indexOf("@") >= 0) {
                    term = extractLast(request.term);
                    if (term.length > 0) {
                        results = $.ui.autocomplete.filter(availableTagsFollowing, term);
                    } else {
                        //results = ['Start typing...'];
                    }
                }
                response(results);
            },
            focus: function() {
                // prevent value inserted on focus
                return false;
            },
            select: function(event, ui) {
                var terms = split(this.innerHTML);
                // remove the current input
                terms.pop();
                // 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;
            }

        });

这是HTML代码

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

这是Chrome检查器的图片:

http://dev.frinki.com/question/2.png

我在 facebook 上查看 是如何完成的,这是 chrome 的检查器在 facebook 中的图片:

3.png(与其他两个图像相同的域和文件夹,对此感到抱歉,这是一个新用户,我不能发布超过 2 个链接)

它 看来当您选择要提及的人时,facebook 会在其使用的范围内创建一个新行,以便将文本与 分开。

好吧,我希望有人可以帮助我。

顺便说一句,我对我的英语感到非常抱歉,我来自南美洲。

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 <b></b> 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:

http://dev.frinki.com/question/1.png

So this is the javascript code im using

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

        }).autocomplete({
            minLength: 0,
            source: function(request, response) {
                var term = request.term,
                    results = [];
                    console.log(term);
                if (term.indexOf("@") >= 0) {
                    term = extractLast(request.term);
                    if (term.length > 0) {
                        results = $.ui.autocomplete.filter(availableTagsFollowing, term);
                    } else {
                        //results = ['Start typing...'];
                    }
                }
                response(results);
            },
            focus: function() {
                // prevent value inserted on focus
                return false;
            },
            select: function(event, ui) {
                var terms = split(this.innerHTML);
                // remove the current input
                terms.pop();
                // 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:

http://dev.frinki.com/question/2.png

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 <b></b>.

Well i hope that someone can help me.

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

千里故人稀 2024-12-11 23:53:11

我只是解释/总结您的想法:您的 div 是可编辑的,用户可以在其中输入内容。在某些情况下,您会呈现一个用户可以选择的列表。一旦做出选择,div 的内容就会被修改以在某个包装元素中包含所选值。之后用户键入,但字符被插入到包装器中而不是包装器之后,对吗?

不良行为的原因是焦点位置。从技术上讲,有一系列选定的内容会被用户键入的字符替换。该范围通常是不可见的,因为它正好跨越 0 个字符。例如,当使用鼠标选择文本时,它会跨越某些内容(可能还有多个元素)。

现在让我们假设以下 HTML:OneTwo。空范围可能位于 b 标记之间。当输入下一个字符 A 时,需要将其插入到文档中 - 但即使范围/插入符号/光标位置已知,也有 3 个可能的目的地:

  • 在第一个 的末尾b: OneATwo
  • 在标签之间: OneATwo
  • 在第二个 b 的开头:OneATwo

您的浏览器选择第一种方法:粗略地说,继续添加到最近添加的元素。

现在,有一个相当复杂的范围系统 指定使用哪种方法来放置新内容。请参阅 Range 简介一个关于设置范围的问题作为示例。

解决问题的总体思路是在新插入的内容之后创建一些空内容。因此最终得到 Mention|。请注意,您需要放置一个空文本节点,而不是管道字符 |,但这无法在 HTML 代码中显示。 DOM 能够处理的不仅仅是序列化的 HTML/HTML 代码!然后您需要选择空文本节点的内容。然后,下一个字符将替换空文本节点(文本节点内选择空内容的范围将被替换,因此该字符将被插入到文本节点中以更精确)。结果是您最终得到 MentionA 假设输入的字符是 A

为了实现这个想法,我建议:

  • 避免使用 innerHTML 并使用 DOM 方法。 innerHTML 无法创建空文本节点。
  • 阅读有关文档对象模型范围的文档小心。

我准备了一个 jsFiddle 来展示基本的实现。请注意,这是一个快速破解,既不支持跨越多个元素的范围,也不支持空父元素。该示例将所有大写字母包装在 b 标记中,这应该足以帮助您入门。

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

var textNode = range.startContainer;    
var text = textNode.data;
// 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
textNode.data = 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');
wrapper.appendChild(document.createTextNode(letter));
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);
selection.addRange(range);

Chrome 对于从偏移量 0 开始的零长度选择失败,并将零长度选择设置为前一个子项的末尾。支持 Chrome 的一个丑陋的黑客可能是插入一些不可见的字符,并在插入下一个字符后将其删除。

Chrome 的问题对于原始问题来说应该不会太重要,因为在包装元素后面插入空格可能是有效的。然后,该范围的起始偏移量将设置为 1,以在空格后插入新内容。

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 = textNode.data;
// 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
textNode.data = 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');
wrapper.appendChild(document.createTextNode(letter));
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);
selection.addRange(range);

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.

萌吟 2024-12-11 23:53:11

我晚了几年,但考虑到您似乎没有找到一个强大的、跨浏览器兼容的解决方案来解决您的问题,我想我应该插话一下。

简单地说,您不无需偏离您的 textarea 实现即可完成您想要做的事情

您可以简单地使用尺寸和位置与 textarea 相同的 div,并放置在 textarea 下,其中包含 textarea 内容的一个版本每个提及的字符串都包含在 span 元素中。您可以通过这些跨度格式化提及字符串。为了使该元素对具有单个元素的外观,textarea 的背景以及“shadow div”的文本、边框和轮廓应设为透明。

如果您想了解如何在提供该功能的现有插件中完成此操作,请查看 Mentionator你正试图效仿。它结构良好、易于理解并且有大量注释,因此您应该很容易理解如何采用这种技术。它也是由您真正维护的:)。

I'm a couple of years late, but considering it doesn't appear as if you've found a robust, cross-browser compatible solution to your problem, I thought I'd chime in.

Simply put, you don't need to stray away from your textarea implementation in order to accomplish what you are trying to do.

You can simply utilize a div, identical in dimension and position to, and placed under the textarea, which contains a version of the textarea content with the string of each mention encased in a span element. You can format the mention strings via these spans. In order to give this element pair the appearance of a single element, the background of the textarea, and the text, border, and outline of the "shadow div" should be made transparent.

Check out Mentionator if you want an idea of how this is accomplished in an existing plug-in which provides the functionality you are trying to emulate. It is well structured, easy to follow, and copiously commented, so you should have little trouble understanding how to adopt such a technique. It is also maintained by yours truly:) .

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文