Javascript 获取范围相对于其父容器的开始和结束偏移量

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/4811822/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-23 14:12:43  来源:igfitidea点击:

Get a range's start and end offset's relative to its parent container

javascripttextrange

提问by Tom Lehman

Suppose I have this HTML element:

假设我有这个 HTML 元素:

<div id="parent">
 Hello everyone! <a>This is my home page</a>
 <p>Bye!</p>
</div>

And the user selects "home" with his mouse.

并且用户用他的鼠标选择“家”。

I want to be able to determine how many characters into #parenthis selection starts (and how many characters from the end of #parenthis selection ends). This should work even if he selects an HTML tag. (And I need it to work in all browsers)

我希望能够确定#parent他的选择开始有多少个字符(以及#parent他的选择结束后有多少个字符结束)。即使他选择了一个 HTML 标签,这也应该有效。(我需要它在所有浏览器中工作)

range.startOffsetlooks promising, but it is an offset relative only to the range's immediate container, and is a character offset only if the container is a text node.

range.startOffset看起来很有希望,但它只是相对于范围的直接容器的偏移量,并且仅当容器是文本节点时才是字符偏移量。

回答by Tim Down

UPDATE

更新

As pointed out in the comments, my original answer (below) only returns the end of the selection or the caret position. It's fairly easy to adapt the code to return a start and an end offset; here's an example that does so:

正如评论中指出的那样,我的原始答案(如下)仅返回选择的结尾或插入符号位置。修改代码以返回开始和结束偏移相当容易;这是一个这样做的例子:

function getSelectionCharacterOffsetWithin(element) {
    var start = 0;
    var end = 0;
    var doc = element.ownerDocument || element.document;
    var win = doc.defaultView || doc.parentWindow;
    var sel;
    if (typeof win.getSelection != "undefined") {
        sel = win.getSelection();
        if (sel.rangeCount > 0) {
            var range = win.getSelection().getRangeAt(0);
            var preCaretRange = range.cloneRange();
            preCaretRange.selectNodeContents(element);
            preCaretRange.setEnd(range.startContainer, range.startOffset);
            start = preCaretRange.toString().length;
            preCaretRange.setEnd(range.endContainer, range.endOffset);
            end = preCaretRange.toString().length;
        }
    } else if ( (sel = doc.selection) && sel.type != "Control") {
        var textRange = sel.createRange();
        var preCaretTextRange = doc.body.createTextRange();
        preCaretTextRange.moveToElementText(element);
        preCaretTextRange.setEndPoint("EndToStart", textRange);
        start = preCaretTextRange.text.length;
        preCaretTextRange.setEndPoint("EndToEnd", textRange);
        end = preCaretTextRange.text.length;
    }
    return { start: start, end: end };
}

function reportSelection() {
  var selOffsets = getSelectionCharacterOffsetWithin( document.getElementById("editor") );
  document.getElementById("selectionLog").innerHTML = "Selection offsets: " + selOffsets.start + ", " + selOffsets.end;
}

window.onload = function() {
  document.addEventListener("selectionchange", reportSelection, false);
  document.addEventListener("mouseup", reportSelection, false);
  document.addEventListener("mousedown", reportSelection, false);
  document.addEventListener("keyup", reportSelection, false);
};
#editor {
  padding: 5px;
  border: solid green 1px;
}
Select something in the content below:

<div id="editor" contenteditable="true">A <i>wombat</i> is a marsupial native to <b>Australia</b></div>
<div id="selectionLog"></div>

Here's a function that will get the character offset of the caret within the specified element; however, this is a naive implementation that will almost certainly have inconsistencies with line breaks, and makes no attempt to deal with text hidden via CSS (I suspect IE will correctly ignore such text while other browsers will not). To handle all this stuff properly would be tricky. I've now attempted itfor my Rangylibrary.

这是一个函数,它将获取指定元素中插入符号的字符偏移量;然而,这是一个天真的实现,几乎肯定会与换行符不一致,并且不会尝试处理通过 CSS 隐藏的文本(我怀疑 IE 会正确忽略此类文本,而其他浏览器不会)。正确处理所有这些东西会很棘手。我现在已经为我的Rangy图书馆尝试了它

Live example: http://jsfiddle.net/TjXEG/900/

现场示例:http: //jsfiddle.net/TjXEG/900/

function getCaretCharacterOffsetWithin(element) {
    var caretOffset = 0;
    var doc = element.ownerDocument || element.document;
    var win = doc.defaultView || doc.parentWindow;
    var sel;
    if (typeof win.getSelection != "undefined") {
        sel = win.getSelection();
        if (sel.rangeCount > 0) {
            var range = win.getSelection().getRangeAt(0);
            var preCaretRange = range.cloneRange();
            preCaretRange.selectNodeContents(element);
            preCaretRange.setEnd(range.endContainer, range.endOffset);
            caretOffset = preCaretRange.toString().length;
        }
    } else if ( (sel = doc.selection) && sel.type != "Control") {
        var textRange = sel.createRange();
        var preCaretTextRange = doc.body.createTextRange();
        preCaretTextRange.moveToElementText(element);
        preCaretTextRange.setEndPoint("EndToEnd", textRange);
        caretOffset = preCaretTextRange.text.length;
    }
    return caretOffset;
}

回答by Cody Crumrine

I know this is a year old, but this post is a top search result for a lot of questions on finding the Caret position and I found this useful.

我知道这是一年前的文章,但这篇文章是有关查找插入符号位置的许多问题的最佳搜索结果,我发现这很有用。

I was trying to use Tim's excellent script above to find the new cursor position after having drag-dropped an element from one position to another in a content editable div. It worked perfectly in FF and IE, but in Chrome, the dragging action highlighted all content between the beginning and end of the drag, which resulted in the returned caretOffsetbeing too large or small (by the length of the selected area).

在内容可编辑的 div 中将元素从一个位置拖放到另一个位置后,我试图使用上面 Tim 的优秀脚本来找到新的光标位置。它在FF和IE中完美运行,但在Chrome中,拖动动作突出显示了拖动开始和结束之间的所有内容,导致返回caretOffset的太大或太小(按所选区域的长度)。

I added a few lines to the first if statement to check if text has been selected and adjust the result accordingly. The new statement is below. Forgive me if it's inappropriate to add this here, as it's not what the OP was trying to do, but as I said, several searches on info related to Caret position led me to this post, so it's (hopefully) likely to help someone else.

我在第一个 if 语句中添加了几行,以检查是否选择了文本并相应地调整结果。新声明如下。如果在此处添加此内容不合适,请原谅我,因为这不是 OP 想要做的事情,但正如我所说,对与 Caret 位置相关的信息进行的几次搜索使我找到了这篇文章,因此(希望)可能会帮助其他人.

Tim's first if statement with added lines(*):

Tim 的第一个 if 语句添加了行 (*):

if (typeof window.getSelection != "undefined") {
  var range = window.getSelection().getRangeAt(0);
  var selected = range.toString().length; // *
  var preCaretRange = range.cloneRange();
  preCaretRange.selectNodeContents(element);
  preCaretRange.setEnd(range.endContainer, range.endOffset);

  if(selected){ // *
    caretOffset = preCaretRange.toString().length - selected; // *
  } else { // *
    caretOffset = preCaretRange.toString().length; 
  } // *
}

回答by Candor

After experimenting a few days I found a approach that looks promising. Because selectNodeContents()does not handle <br>tags correctly, I wrote a custom algorithm to determine the text length of each nodeinside a contenteditable. To calculate e.g. the selection start, I sum up the text lengths of all preceding nodes. That way, I can handle (multiple) line breaks:

经过几天的试验,我发现了一种看起来很有希望的方法。因为selectNodeContents()不处理<br>正确的标签,我写了一个自定义的算法来确定每个文本长度nodecontenteditable。为了计算例如选择开始,我总结了所有前面节点的文本长度。这样,我可以处理(多个)换行符:

var editor = null;
var output = null;

const getTextSelection = function (editor) {
    const selection = window.getSelection();

    if (selection != null && selection.rangeCount > 0) {
        const range = selection.getRangeAt(0);

        return {
            start: getTextLength(editor, range.startContainer, range.startOffset),
            end: getTextLength(editor, range.endContainer, range.endOffset)
        };
    } else
        return null;
}

const getTextLength = function (parent, node, offset) {
    var textLength = 0;

    if (node.nodeName == '#text')
        textLength += offset;
    else for (var i = 0; i < offset; i++)
        textLength += getNodeTextLength(node.childNodes[i]);

    if (node != parent)
        textLength += getTextLength(parent, node.parentNode, getNodeOffset(node));

    return textLength;
}

const getNodeTextLength = function (node) {
    var textLength = 0;

    if (node.nodeName == 'BR')
        textLength = 1;
    else if (node.nodeName == '#text')
        textLength = node.nodeValue.length;
    else if (node.childNodes != null)
        for (var i = 0; i < node.childNodes.length; i++)
            textLength += getNodeTextLength(node.childNodes[i]);

    return textLength;
}

const getNodeOffset = function (node) {
    return node == null ? -1 : 1 + getNodeOffset(node.previousSibling);
}

window.onload = function () {
    editor = document.querySelector('.editor');
    output = document.querySelector('#output');

    document.addEventListener('selectionchange', handleSelectionChange);
}

const handleSelectionChange = function () {
    if (isEditor(document.activeElement)) {
        const textSelection = getTextSelection(document.activeElement);

        if (textSelection != null) {
            const text = document.activeElement.innerText;
            const selection = text.slice(textSelection.start, textSelection.end);
            print(`Selection: [${selection}] (Start: ${textSelection.start}, End: ${textSelection.end})`);
        } else
            print('Selection is null!');
    } else
        print('Select some text above');
}

const isEditor = function (element) {
    return element != null && element.classList.contains('editor');
}

const print = function (message) {
    if (output != null)
        output.innerText = message;
    else
        console.log('output is null!');
}
* {
    font-family: 'Georgia', sans-serif;
    padding: 0;
    margin: 0;
}

body {
    margin: 16px;
}

.p {
    font-size: 16px;
    line-height: 24px;
    padding: 0 2px;
}

.editor {
    border: 1px solid #0000001e;
    border-radius: 2px;
    white-space: pre-wrap;
}

#output {
    margin-top: 16px;
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="./script.js" async></script>
    <link href="./stylesheet.css" rel="stylesheet">
    <title>Caret Position</title>
</head>
<body>
    <p class="editor" contenteditable="true"><em>Write<br></em><br>some <br>awesome <b><em>text </em></b>here...</p>
    <p id="output">Select some text above</p>
</body>
</html>