简体   繁体   English

javascript:带有html标签的focusOffset

[英]javascript : focusOffset with html tags

I have a contenteditable div as follow (| = cursor position):我有一个 contenteditable div 如下(| = 光标位置):

<div id="mydiv" contenteditable="true">lorem ipsum <spanclass="highlight">indol|or sit</span> amet consectetur <span class='tag'>adipiscing</span> elit</div>

I would like to get the current cursor position including html tags.我想获取当前光标位置,包括 html 标签。 My code :我的代码:

var offset = document.getSelection().focusOffset;

Offset is returning 5 (full text from the last tag) but i need it to handle html tags. Offset 返回 5(来自最后一个标签的全文),但我需要它来处理 html 标签。 The expected return value is 40. The code has to work with all recents browsers.预期的返回值为 40。该代码必须适用于所有最近浏览器。 (i also checked this : window.getSelection() offset with HTML tags? but it doesn't answer my question). (我还检查了这个: window.getSelection() offset with HTML tags?但它没有回答我的问题)。 Any ideas ?有任何想法吗 ?

EDIT: This is an old answer that doesn't work for OP's requirement of having nodes with the same text.编辑:这是一个旧答案,不适用于 OP 要求节点具有相同文本的要求。 But it's cleaner and lighter if you don't have that requirement.但如果你没有这个要求,它会更干净、更轻便。

Here is one option that you can use and that works in all major browsers:这是您可以使用并且适用于所有主要浏览器的一个选项:

  1. Get the offset of the caret within its node ( document.getSelection().anchorOffset )获取插入符号在其节点内的偏移量( document.getSelection().anchorOffset
  2. Get the text of the node in which the caret is located ( document.getSelection().anchorNode.data )获取插入符号所在节点的文本( document.getSelection().anchorNode.data
  3. Get the offset of that text within #mydiv by using indexOf()使用indexOf()#mydiv获取该文本的偏移量
  4. Add the values obtained in 1 and 3, to get the offset of the caret within the div.将 1 和 3 中获得的值相加,以获得 div 中插入符号的偏移量。

The code would look like this for your particular case:对于您的特定情况,代码如下所示:

var offset = document.getSelection().anchorOffset;
var text = document.getSelection().anchorNode.data;
var textOffset = $("#mydiv").html().indexOf( text );

offsetCaret = textOffset + offset;

You can see a working demo on this JSFiddle (view the console to see the results).您可以在此 JSFiddle 上看到一个工作演示(查看控制台以查看结果)。

And a more generic version of the function (that allows to pass the div as a parameter, so it can be used with different contenteditable ) on this other JSFiddle :还有一个更通用的函数版本(允许将div作为参数传递,因此它可以与不同的contenteditable一起使用)在另一个 JSFiddle 上

function getCaretHTMLOffset(obj) {

    var offset = document.getSelection().anchorOffset;
    var text = document.getSelection().anchorNode.data;
    var textOffset = obj.innerHTML.indexOf( text );

    return textOffset + offset;

}

About this answer关于这个答案

  • It will work in all recent browsers as requested (tested on Chrome 42, Firefox 37, and Explorer 11).它将按要求在所有最近的浏览器中工作(在 Chrome 42、Firefox 37 和 Explorer 11 上测试)。
  • It is short and light, and doesn't require any external library (not even jQuery)它短小精悍,不需要任何外部库(甚至不需要 jQuery)
  • Issue : If you have different nodes with the same text, it may return the offset of the first occurrence instead of the real position of the caret.问题:如果具有相同文本的不同节点,它可能会返回第一次出现的偏移量而不是插入符号的实际位置。

Another way to do it is by adding a temporary marker in the DOM and calculating the offset from this marker.另一种方法是在 DOM 中添加一个临时标记并计算与该标记的偏移量。 The algorithm looks for the HTML serialization of the marker (its outerHTML ) within the inner serialization (the innerHTML ) of the div of interest.该算法在感兴趣的div的内部序列化( innerHTML )中查找标记(其outerHTML )的 HTML 序列化。 Repeated text is not a problem with this solution.重复文本不是这个解决方案的问题。

For this to work, the marker's serialization must be unique within its div.为此,标记的序列化在其 div 中必须是唯一的。 You cannot control what users type into a field but you can control what you put into the DOM so this should not be difficult to achieve.您无法控制用户在字段中输入的内容,但您可以控制放入 DOM 的内容,因此这应该不难实现。 In my example, the marker is made unique statically: by choosing a class name unlikely to cause a clash ahead of time.在我的示例中,标记是静态唯一的:通过选择一个不太可能提前引起冲突的类名。 It would also be possible to do it dynamically, by checking the DOM and changing the class until it is unique.也可以通过检查 DOM 并更改类直到它是唯一的来动态执行此操作。

I have a fiddle for it (derived from Alvaro Montoro's own fiddle).我有一个小提琴(源自阿尔瓦罗蒙托罗自己的小提琴)。 The main part is:主要部分是:

function getOffset() {

    if ($("." + unique).length)
        throw new Error("marker present in document; or the unique class is not unique");

    // We could also use rangy.getSelection() but there's no reason here to do this.
    var sel = document.getSelection();

    if (!sel.rangeCount)
        return; // No ranges.

    if (!sel.isCollapsed)
        return; // We work only with collapsed selections.

    if (sel.rangeCount > 1)
        throw new Error("can't handle multiple ranges");

    var range = sel.getRangeAt(0);
    var saved = rangy.serializeSelection();
    // See comment below.
    $mydiv[0].normalize();

    range.insertNode($marker[0]);
    var offset = $mydiv.html().indexOf($marker[0].outerHTML);
    $marker.remove();

    // Normalizing before and after ensures that the DOM is in the same shape before 
    // and after the insertion and removal of the marker.
    $mydiv[0].normalize();
    rangy.deserializeSelection(saved);

    return offset;
}

As you can see, the code has to compensate for the addition and removal of the marker into the DOM because this causes the current selection to get lost:如您所见,代码必须补偿在 DOM 中添加和删除标记,因为这会导致当前选择丢失:

  1. Rangy is used to save the selection and restore it afterwards. Rangy用于保存选择并在之后恢复它。 Note that the save and restore could be done with something lighter than Rangy but I did not want to load the answer with minutia.请注意,保存和恢复可以用比 Rangy 更轻的东西来完成,但我不想用细节加载答案。 If you decide to use Rangy for this task, please read the documentation because it is possible to optimize the serialization and deserialization.如果您决定使用 Rangy 执行此任务,请阅读文档,因为可以优化序列化和反序列化。

  2. For Rangy to work, the DOM must be in exactly the same state before and after the save.为了让 Rangy 工作,DOM 必须在保存前后处于完全相同的状态。 This is why normalize() is called before we add the marker and after we remove it.这就是在添加标记之前和删除标记之后调用normalize()原因。 What this does is merge immediately adjacent text nodes into a single text node.这样做是将紧邻的文本节点合并为一个文本节点。 The issue is that adding a marker to the DOM can cause a text node to be broken into two new text nodes.问题是向 DOM 添加标记会导致文本节点被分成两个新的文本节点。 This causes the selection to be lost and, if not undone with a normalization, would cause Rangy to be unable to restore the selection.这会导致选择丢失,如果不通过规范化撤消,将导致 Rangy 无法恢复选择。 Again, something lighter than calling normalize could do the trick but I did not want to load the answer with minutia.同样,比调用normalize更轻松的方法可以解决问题,但我不想用细节加载答案。

NOTE: This solution works even in nodes with repeated text, but it detects html entities (eg: &nbsp; ) as only one character.注意:此解决方案甚至适用于具有重复文本的节点,但它仅将 html 实体(例如: &nbsp; )检测为一个字符。

I came up with a completely different solution based on processing the nodes.我想出了一个基于处理节点的完全不同的解决方案。 It is not as clean as the old answer ( see other answer ), but it works fine even when there are nodes with the same text (OP's requirement).它不像旧答案那么干净(请参阅其他答案),但即使存在具有相同文本的节点(OP 要求),它也能正常工作。

This is a description of how it works:这是它如何工作的描述:

  1. Create a stack with all the parent elements of the node in which the caret is located.使用插入符号所在节点的所有父元素创建一个堆栈。
  2. While the stack is not empty, traverse the nodes of the containing element (initially the content editable div).当堆栈不为空时,遍历包含元素的节点(最初是内容可编辑的 div)。
  3. If the node is not the same one at the top of the stack, add its size to the offset.如果节点与堆栈顶部的节点不同,则将其大小添加到偏移量中。
  4. If the node is the same as the one at the top of the stack: pop it from the stack, go to step 2.如果节点与栈顶节点相同:从栈中弹出,转步骤2。

The code is like this:代码是这样的:

function getCaretOffset(contentEditableDiv) {

    // read the node in which the caret is and store it in a stack
    var aux = document.getSelection().anchorNode;
    var stack = [ aux ];

    // add the parents to the stack until we get to the content editable div
    while ($(aux).parent()[0] != contentEditableDiv) { aux = $(aux).parent()[0]; stack.push(aux); }

    // traverse the contents of the editable div until we reach the one with the caret
    var offset   = 0;
    var currObj  = contentEditableDiv;
    var children = $(currObj).contents();
    while (stack.length) {
        // add the lengths of the previous "siblings" to the offset
        for (var x = 0; x < children.length; x++) {
            if (children[x] == stack[stack.length-1]) {
                // if the node is not a text node, then add the size of the opening tag
                if (children[x].nodeType != 3) { offset += $(children[x])[0].outerHTML.indexOf(">") + 1; }
                break;
            } else {
                if (children[x].nodeType == 3) {
                    // if it's a text node, add it's size to the offset
                    offset += children[x].length;
                } else {
                    // if it's a tag node, add it's size + the size of the tags
                    offset += $(children[x])[0].outerHTML.length;
                }
            }
        }

        // move to a more inner container
        currObj  = stack.pop();
        children = $(currObj).contents();
    }

    // finally add the offset within the last node
    offset += document.getSelection().anchorOffset;

    return offset;
}

You can see a working demo on this JSFiddle .你可以在这个 JSFiddle 上看到一个工作演示


About this answer:关于这个答案:

  • It works in all major browsers.它适用于所有主要浏览器。
  • It is light and doesn't require external libraries (apart from jQuery)它很轻,不需要外部库(除了 jQuery)
  • It has an issue : html entities like &nbsp;它有一个问题:像&nbsp;这样的 html 实体&nbsp; are counted as one character only.仅计为一个字符。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM