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

前端 未结 4 1914
独厮守ぢ
独厮守ぢ 2020-11-22 05:19

Suppose I have this HTML element:

Hello everyone! This is my home page

Bye!

相关标签:
4条回答
  • 2020-11-22 05:30

    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 node inside 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:

    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>

    0 讨论(0)
  • 2020-11-22 05:40

    This solution works by counting length of text content of previous siblings walking back up to the parent container. It probably doesn't cover all edge cases, although it does handle nested tags of any depth, but it's a good, simple place to start from if you have a similar need.

      calculateTotalOffset(node, offset) {
        let total = offset
        let curNode = node
    
        while (curNode.id != 'parent') {
          if(curNode.previousSibling) {
            total += curNode.previousSibling.textContent.length
    
            curNode = curNode.previousSibling
          } else {
            curNode = curNode.parentElement
          }
        }
    
       return total
     }
    
     // after selection
    
    let start = calculateTotalOffset(range.startContainer, range.startOffset)
    let end = calculateTotalOffset(range.endContainer, range.endOffset)
    
    0 讨论(0)
  • 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 caretOffset being too large or small (by the length of the selected area).

    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.

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

    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; 
      } // *
    }
    
    0 讨论(0)
  • 2020-11-22 05:51

    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 it for my Rangy library.

    Live example: 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;
    }
    
    0 讨论(0)
提交回复
热议问题