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

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

假设我有这个HTML元素:

1
2
3
4
 Hello everyone! This is my home page
 <p>
Bye!
</p>

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

我希望能够确定他的选择开始到#parent中有多少个字符(以及他的选择结束到#parent末尾有多少个字符)。 即使他选择了HTML标签,这也应该起作用。 (并且我需要它在所有浏览器中都能正常工作)

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


更新

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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);
};
1
2
3
4
#editor {
  padding: 5px;
  border: solid green 1px;
}

1
2
3
Select something in the content below:

A wombat is a marsupial native to Australia

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

实时示例:http://jsfiddle.net/TjXEG/900/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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;
}


我知道这已经一岁了,但是这篇文章是关于找到Caret职位的许多问题的最佳搜索结果,我发现这很有用。

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

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

蒂姆的第一条if语句,增加了行(*):

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
  } // *
}


经过几天的试验,我发现了一种很有前途的方法。因为selectNodeContents()无法正确处理标记,所以我编写了一个自定义算法来确定contenteditable中每个node的文本长度。计算例如选择开始时,我总结了前面所有节点的文本长度。这样,我可以处理(多个)换行符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
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!');
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
* {
    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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!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>
    <link href="./stylesheet.css" rel="stylesheet">
    Caret Position
</head>
<body>
    <p class="editor" contenteditable="true">Writesome awesome text here...
</p>
    <p id="output">Select some text above
</p>
</body>
</html>