获取内容可编辑插入符位置

发布于 2024-09-28 08:27:05 字数 371 浏览 7 评论 0原文

我找到了大量关于如何在 contentEditable 元素中设置插入符位置的良好跨浏览器答案,但没有找到关于如何获取的答案> 将插入符号位置放在首位。

我想要做的是知道 keyup 上 div 中的插入符号位置。因此,当用户输入文本时,我可以随时知道 contentEditable 元素中的插入符位置。

<div id="contentBox" contentEditable="true"></div>

$('#contentbox').keyup(function() { 
    // ... ? 
});

I'm finding tons of good, cross-browser answers on how to set the caret position in a contentEditable element, but none on how to get the caret position in the first place.

What I want to do is know the caret position within a div on keyup. So, when the user is typing text, I can, at any point, know the caret position within the contentEditable element.

<div id="contentBox" contentEditable="true"></div>

$('#contentbox').keyup(function() { 
    // ... ? 
});

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

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

发布评论

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

评论(18

初心 2024-10-05 08:27:05

以下代码假设:

  • 可编辑
    内始终有一个文本节点,并且没有其他节点
  • 可编辑 div 没有设置 CSS white-space 属性 :

如果您需要一种更通用的方法来处理嵌套元素的内容,请尝试以下答案:

https //stackoverflow.com/a/4812022/96100

代码:

function getCaretPosition(editableDiv) {
  var caretPos = 0,
    sel, range;
  if (window.getSelection) {
    sel = window.getSelection();
    if (sel.rangeCount) {
      range = sel.getRangeAt(0);
      if (range.commonAncestorContainer.parentNode == editableDiv) {
        caretPos = range.endOffset;
      }
    }
  } else if (document.selection && document.selection.createRange) {
    range = document.selection.createRange();
    if (range.parentElement() == editableDiv) {
      var tempEl = document.createElement("span");
      editableDiv.insertBefore(tempEl, editableDiv.firstChild);
      var tempRange = range.duplicate();
      tempRange.moveToElementText(tempEl);
      tempRange.setEndPoint("EndToEnd", range);
      caretPos = tempRange.text.length;
    }
  }
  return caretPos;
}
#caretposition {
  font-weight: bold;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<div id="contentbox" contenteditable="true">Click me and move cursor with keys or mouse</div>
<div id="caretposition">0</div>
<script>
  var update = function() {
    $('#caretposition').html(getCaretPosition(this));
  };
  $('#contentbox').on("mousedown mouseup keydown keyup", update);
</script>

The following code assumes:

  • There is always a single text node within the editable <div> and no other nodes
  • The editable div does not have the CSS white-space property set to pre

If you need a more general approach that will work content with nested elements, try this answer:

https://stackoverflow.com/a/4812022/96100

Code:

function getCaretPosition(editableDiv) {
  var caretPos = 0,
    sel, range;
  if (window.getSelection) {
    sel = window.getSelection();
    if (sel.rangeCount) {
      range = sel.getRangeAt(0);
      if (range.commonAncestorContainer.parentNode == editableDiv) {
        caretPos = range.endOffset;
      }
    }
  } else if (document.selection && document.selection.createRange) {
    range = document.selection.createRange();
    if (range.parentElement() == editableDiv) {
      var tempEl = document.createElement("span");
      editableDiv.insertBefore(tempEl, editableDiv.firstChild);
      var tempRange = range.duplicate();
      tempRange.moveToElementText(tempEl);
      tempRange.setEndPoint("EndToEnd", range);
      caretPos = tempRange.text.length;
    }
  }
  return caretPos;
}
#caretposition {
  font-weight: bold;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<div id="contentbox" contenteditable="true">Click me and move cursor with keys or mouse</div>
<div id="caretposition">0</div>
<script>
  var update = function() {
    $('#caretposition').html(getCaretPosition(this));
  };
  $('#contentbox').on("mousedown mouseup keydown keyup", update);
</script>

与君绝 2024-10-05 08:27:05

我在其他答案中没有看到的一些问题:

  1. 该元素可以包含多个级别的子节点(例如,具有子节点的子节点具有子节点...)
  2. 选择可以包含不同的开始和结束位置(例如,选择了多个字符)
  3. 包含插入符开始/结束的节点可能不是该元素或其直接子元素

以下是获取开始和结束位置作为元素 textContent 值的偏移量的方法:

// node_walk: walk the element tree, stop when func(node) returns false
function node_walk(node, func) {
  var result = func(node);
  for(node = node.firstChild; result !== false && node; node = node.nextSibling)
    result = node_walk(node, func);
  return result;
};

// getCaretPosition: return [start, end] as offsets to elem.textContent that
//   correspond to the selected portion of text
//   (if start == end, caret is at given position and no text is selected)
function getCaretPosition(elem) {
  var sel = window.getSelection();
  var cum_length = [0, 0];

  if(sel.anchorNode == elem)
    cum_length = [sel.anchorOffset, sel.extentOffset];
  else {
    var nodes_to_find = [sel.anchorNode, sel.extentNode];
    if(!elem.contains(sel.anchorNode) || !elem.contains(sel.extentNode))
      return undefined;
    else {
      var found = [0,0];
      var i;
      node_walk(elem, function(node) {
        for(i = 0; i < 2; i++) {
          if(node == nodes_to_find[i]) {
            found[i] = true;
            if(found[i == 0 ? 1 : 0])
              return false; // all done
          }
        }

        if(node.textContent && !node.firstChild) {
          for(i = 0; i < 2; i++) {
            if(!found[i])
              cum_length[i] += node.textContent.length;
          }
        }
      });
      cum_length[0] += sel.anchorOffset;
      cum_length[1] += sel.extentOffset;
    }
  }
  if(cum_length[0] <= cum_length[1])
    return cum_length;
  return [cum_length[1], cum_length[0]];
}

A few wrinkles that I don't see being addressed in other answers:

  1. the element can contain multiple levels of child nodes (e.g. child nodes that have child nodes that have child nodes...)
  2. a selection can consist of different start and end positions (e.g. multiple chars are selected)
  3. the node containing a Caret start/end may not be either the element or its direct children

Here's a way to get start and end positions as offsets to the element's textContent value:

// node_walk: walk the element tree, stop when func(node) returns false
function node_walk(node, func) {
  var result = func(node);
  for(node = node.firstChild; result !== false && node; node = node.nextSibling)
    result = node_walk(node, func);
  return result;
};

// getCaretPosition: return [start, end] as offsets to elem.textContent that
//   correspond to the selected portion of text
//   (if start == end, caret is at given position and no text is selected)
function getCaretPosition(elem) {
  var sel = window.getSelection();
  var cum_length = [0, 0];

  if(sel.anchorNode == elem)
    cum_length = [sel.anchorOffset, sel.extentOffset];
  else {
    var nodes_to_find = [sel.anchorNode, sel.extentNode];
    if(!elem.contains(sel.anchorNode) || !elem.contains(sel.extentNode))
      return undefined;
    else {
      var found = [0,0];
      var i;
      node_walk(elem, function(node) {
        for(i = 0; i < 2; i++) {
          if(node == nodes_to_find[i]) {
            found[i] = true;
            if(found[i == 0 ? 1 : 0])
              return false; // all done
          }
        }

        if(node.textContent && !node.firstChild) {
          for(i = 0; i < 2; i++) {
            if(!found[i])
              cum_length[i] += node.textContent.length;
          }
        }
      });
      cum_length[0] += sel.anchorOffset;
      cum_length[1] += sel.extentOffset;
    }
  }
  if(cum_length[0] <= cum_length[1])
    return cum_length;
  return [cum_length[1], cum_length[0]];
}
记忆で 2024-10-05 08:27:05

参加聚会有点晚了,但以防其他人陷入困境。过去两天我发现的谷歌搜索都没有找到任何有用的东西,但我想出了一个简洁而优雅的解决方案,无论您有多少嵌套标签,它都将始终有效:

function cursor_position() {
    var sel = document.getSelection();
    sel.modify("extend", "backward", "paragraphboundary");
    var pos = sel.toString().length;
    if(sel.anchorNode != undefined) sel.collapseToEnd();

    return pos;
}

// Demo:
var elm = document.querySelector('[contenteditable]');
elm.addEventListener('click', printCaretPosition)
elm.addEventListener('keydown', printCaretPosition)

function printCaretPosition(){
  console.log( cursor_position(), 'length:', this.textContent.trim().length )
}
<div contenteditable>some text here <i>italic text here</i> some other text here <b>bold text here</b> end of text</div>

它一路选择回到段落的开头,然后计算字符串的长度以获取当前位置,然后撤消选择以使光标返回到当前位置。如果您想对整个文档(多个段落)执行此操作,请将 paragraphboundary 更改为 documentboundary 或适合您情况的任何粒度。查看 API 更多详细信息。干杯! :)

Kinda late to the party, but in case anyone else is struggling. None of the Google searches I've found for the past two days have come up with anything that works, but I came up with a concise and elegant solution that will always work no matter how many nested tags you have:

function cursor_position() {
    var sel = document.getSelection();
    sel.modify("extend", "backward", "paragraphboundary");
    var pos = sel.toString().length;
    if(sel.anchorNode != undefined) sel.collapseToEnd();

    return pos;
}

// Demo:
var elm = document.querySelector('[contenteditable]');
elm.addEventListener('click', printCaretPosition)
elm.addEventListener('keydown', printCaretPosition)

function printCaretPosition(){
  console.log( cursor_position(), 'length:', this.textContent.trim().length )
}
<div contenteditable>some text here <i>italic text here</i> some other text here <b>bold text here</b> end of text</div>

It selects all the way back to the beginning of the paragraph and then counts the length of the string to get the current position and then undoes the selection to return the cursor to the current position. If you want to do this for an entire document (more than one paragraph), then change paragraphboundary to documentboundary or whatever granularity for your case. Check out the API for more details. Cheers! :)

灼痛 2024-10-05 08:27:05
$("#editable").on('keydown keyup mousedown mouseup',function(e){
		   
       if($(window.getSelection().anchorNode).is($(this))){
    	  $('#position').html('0')
       }else{
         $('#position').html(window.getSelection().anchorOffset);
       }
 });
body{
  padding:40px;
}
#editable{
  height:50px;
  width:400px;
  border:1px solid #000;
}
#editable p{
  margin:0;
  padding:0;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.1/jquery.min.js"></script>
<div contenteditable="true" id="editable">move the cursor to see position</div>
<div>
position : <span id="position"></span>
</div>

$("#editable").on('keydown keyup mousedown mouseup',function(e){
		   
       if($(window.getSelection().anchorNode).is($(this))){
    	  $('#position').html('0')
       }else{
         $('#position').html(window.getSelection().anchorOffset);
       }
 });
body{
  padding:40px;
}
#editable{
  height:50px;
  width:400px;
  border:1px solid #000;
}
#editable p{
  margin:0;
  padding:0;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.1/jquery.min.js"></script>
<div contenteditable="true" id="editable">move the cursor to see position</div>
<div>
position : <span id="position"></span>
</div>

无需解释 2024-10-05 08:27:05

window.getSelection - 与 - document.selection

这对我有用:

function getCaretCharOffset(element) {
  var caretOffset = 0;

  if (window.getSelection) {
    var range = window.getSelection().getRangeAt(0);
    var preCaretRange = range.cloneRange();
    preCaretRange.selectNodeContents(element);
    preCaretRange.setEnd(range.endContainer, range.endOffset);
    caretOffset = preCaretRange.toString().length;
  } 

  else if (document.selection && document.selection.type != "Control") {
    var textRange = document.selection.createRange();
    var preCaretTextRange = document.body.createTextRange();
    preCaretTextRange.moveToElementText(element);
    preCaretTextRange.setEndPoint("EndToEnd", textRange);
    caretOffset = preCaretTextRange.text.length;
  }

  return caretOffset;
}


// Demo:
var elm = document.querySelector('[contenteditable]');
elm.addEventListener('click', printCaretPosition)
elm.addEventListener('keydown', printCaretPosition)

function printCaretPosition(){
  console.log( getCaretCharOffset(elm), 'length:', this.textContent.trim().length )
}
<div contenteditable>some text here <i>italic text here</i> some other text here <b>bold text here</b> end of text</div>

调用行取决于事件类型,对于按键事件使用此:

getCaretCharOffsetInDiv(e.target) + ($(window.getSelection().getRangeAt(0).startContainer.parentNode).index());

对于鼠标事件使用此:

getCaretCharOffsetInDiv(e.target.parentElement) + ($(e.target).index())

在这两种情况下,我通过添加目标索引来处理断线

window.getSelection - vs - document.selection

This one works for me:

function getCaretCharOffset(element) {
  var caretOffset = 0;

  if (window.getSelection) {
    var range = window.getSelection().getRangeAt(0);
    var preCaretRange = range.cloneRange();
    preCaretRange.selectNodeContents(element);
    preCaretRange.setEnd(range.endContainer, range.endOffset);
    caretOffset = preCaretRange.toString().length;
  } 

  else if (document.selection && document.selection.type != "Control") {
    var textRange = document.selection.createRange();
    var preCaretTextRange = document.body.createTextRange();
    preCaretTextRange.moveToElementText(element);
    preCaretTextRange.setEndPoint("EndToEnd", textRange);
    caretOffset = preCaretTextRange.text.length;
  }

  return caretOffset;
}


// Demo:
var elm = document.querySelector('[contenteditable]');
elm.addEventListener('click', printCaretPosition)
elm.addEventListener('keydown', printCaretPosition)

function printCaretPosition(){
  console.log( getCaretCharOffset(elm), 'length:', this.textContent.trim().length )
}
<div contenteditable>some text here <i>italic text here</i> some other text here <b>bold text here</b> end of text</div>

The calling line depends on event type, for key event use this:

getCaretCharOffsetInDiv(e.target) + ($(window.getSelection().getRangeAt(0).startContainer.parentNode).index());

for mouse event use this:

getCaretCharOffsetInDiv(e.target.parentElement) + ($(e.target).index())

on these two cases I take care for break lines by adding the target index

小猫一只 2024-10-05 08:27:05
function getCaretPosition() {
    var x = 0;
    var y = 0;
    var sel = window.getSelection();
    if(sel.rangeCount) {
        var range = sel.getRangeAt(0).cloneRange();
        if(range.getClientRects()) {
        range.collapse(true);
        var rect = range.getClientRects()[0];
        if(rect) {
            y = rect.top;
            x = rect.left;
        }
        }
    }
    return {
        x: x,
        y: y
    };
}
function getCaretPosition() {
    var x = 0;
    var y = 0;
    var sel = window.getSelection();
    if(sel.rangeCount) {
        var range = sel.getRangeAt(0).cloneRange();
        if(range.getClientRects()) {
        range.collapse(true);
        var rect = range.getClientRects()[0];
        if(rect) {
            y = rect.top;
            x = rect.left;
        }
        }
    }
    return {
        x: x,
        y: y
    };
}
傾旎 2024-10-05 08:27:05

试试这个:

Caret.js
从文本字段获取插入符位置和偏移量

https://github.com/ichord/Caret.js

演示:
http://ichord.github.com/Caret.js

Try this:

Caret.js
Get caret postion and offset from text field

https://github.com/ichord/Caret.js

demo:
http://ichord.github.com/Caret.js

赤濁 2024-10-05 08:27:05

因为这花了我很长时间才弄清楚如何使用新的 window.getSelection< /a> API 我将与后代分享。请注意,MDN 建议对 window.getSelection 提供更广泛的支持,但是,您的情况可能会有所不同。

const getSelectionCaretAndLine = () => {
    // our editable div
    const editable = document.getElementById('editable');

    // collapse selection to end
    window.getSelection().collapseToEnd();

    const sel = window.getSelection();
    const range = sel.getRangeAt(0);

    // get anchor node if startContainer parent is editable
    let selectedNode = editable === range.startContainer.parentNode
      ? sel.anchorNode 
      : range.startContainer.parentNode;

    if (!selectedNode) {
        return {
            caret: -1,
            line: -1,
        };
    }

    // select to top of editable
    range.setStart(editable.firstChild, 0);

    // do not use 'this' sel anymore since the selection has changed
    const content = window.getSelection().toString();
    const text = JSON.stringify(content);
    const lines = (text.match(/\\n/g) || []).length + 1;

    // clear selection
    window.getSelection().collapseToEnd();

    // minus 2 because of strange text formatting
    return {
        caret: text.length - 2, 
        line: lines,
    }
} 

这是一个在按键时触发的 jsfiddle 。但请注意,快速方向键按下以及快速删除似乎是跳过事件。

As this took me forever to figure out using the new window.getSelection API I am going to share for posterity. Note that MDN suggests there is wider support for window.getSelection, however, your mileage may vary.

const getSelectionCaretAndLine = () => {
    // our editable div
    const editable = document.getElementById('editable');

    // collapse selection to end
    window.getSelection().collapseToEnd();

    const sel = window.getSelection();
    const range = sel.getRangeAt(0);

    // get anchor node if startContainer parent is editable
    let selectedNode = editable === range.startContainer.parentNode
      ? sel.anchorNode 
      : range.startContainer.parentNode;

    if (!selectedNode) {
        return {
            caret: -1,
            line: -1,
        };
    }

    // select to top of editable
    range.setStart(editable.firstChild, 0);

    // do not use 'this' sel anymore since the selection has changed
    const content = window.getSelection().toString();
    const text = JSON.stringify(content);
    const lines = (text.match(/\\n/g) || []).length + 1;

    // clear selection
    window.getSelection().collapseToEnd();

    // minus 2 because of strange text formatting
    return {
        caret: text.length - 2, 
        line: lines,
    }
} 

Here is a jsfiddle that fires on keyup. Note however, that rapid directional key presses, as well as rapid deletion seems to be skip events.

双马尾 2024-10-05 08:27:05
//global savedrange variable to store text range in
var savedrange = null;

function getSelection()
{
    var savedRange;
    if(window.getSelection && window.getSelection().rangeCount > 0) //FF,Chrome,Opera,Safari,IE9+
    {
        savedRange = window.getSelection().getRangeAt(0).cloneRange();
    }
    else if(document.selection)//IE 8 and lower
    { 
        savedRange = document.selection.createRange();
    }
    return savedRange;
}

$('#contentbox').keyup(function() { 
    var currentRange = getSelection();
    if(window.getSelection)
    {
        //do stuff with standards based object
    }
    else if(document.selection)
    { 
        //do stuff with microsoft object (ie8 and lower)
    }
});

注意:范围对象本身可以存储在变量中,并且可以随时重新选择,除非 contenteditable div 的内容发生更改。

IE 8 及更低版本参考:
http://msdn.microsoft.com/en-us/ Library/ms535872(VS.85).aspx

标准(所有其他)浏览器的参考:
https://developer.mozilla.org/en/DOM/range(这是 Mozilla文档,但代码也适用于 chrome、safari、opera 和 ie9)

//global savedrange variable to store text range in
var savedrange = null;

function getSelection()
{
    var savedRange;
    if(window.getSelection && window.getSelection().rangeCount > 0) //FF,Chrome,Opera,Safari,IE9+
    {
        savedRange = window.getSelection().getRangeAt(0).cloneRange();
    }
    else if(document.selection)//IE 8 and lower
    { 
        savedRange = document.selection.createRange();
    }
    return savedRange;
}

$('#contentbox').keyup(function() { 
    var currentRange = getSelection();
    if(window.getSelection)
    {
        //do stuff with standards based object
    }
    else if(document.selection)
    { 
        //do stuff with microsoft object (ie8 and lower)
    }
});

Note: the range object its self can be stored in a variable, and can be re-selected at any time unless the contents of the contenteditable div change.

Reference for IE 8 and lower:
http://msdn.microsoft.com/en-us/library/ms535872(VS.85).aspx

Reference for standards (all other) browsers:
https://developer.mozilla.org/en/DOM/range (its the mozilla docs, but code works in chrome, safari, opera and ie9 too)

执笏见 2024-10-05 08:27:05

尝试通过这种方式获取从 ContentEditable Div 中插入符号的位置

描述:

  1. 我为 Angular 编写了这段代码,但它也适用于本机 HTML。
  2. 该代码仅返回可编辑 div 内的 SPAN 元素的插入符位置。

我的代码:

private getCaretPosition() {
   let caretRevCount = 0;
   if (window.getSelection) {
      const selection = window.getSelection();
      const currentNode = selection.focusNode.parentNode;
      caretRevCount = selection.focusOffset;
      let previousNode = currentNode.previousSibling;
      while(previousNode && previousNode.nodeName === 'SPAN') { 
      // you can check specific element
      caretRevCount += previousNode.textContent.length;
      previousNode = previousNode.previousSibling;
      }
    }
    return caretRevCount;
}

代码如何工作:

示例场景:“您好,这|是示例文本”。

插入符位置:位于“this”文本的末尾。

  1. 最初,从 window.getSelection() 方法获取插入符号所在的选择区域。
  2. selection.focusOffSet 仅返回 currentNode 文本长度。在例如情况下,currentNode是“this”。它向 caretRevCount 返回 4。
  3. 我的方法是从当前节点回溯。因此,我循环之前的节点 ["there, ", "Hi"] 并将其文本长度添加到 caretRevCount。
  4. 最后,循环完成后 caretRevCount 返回一个总和值,即 caretPosition

Try this way to get the Caret position from ContentEditable Div.

Description:

  1. I have written this code for Angular but it also works for native HTML.
  2. The code returns caret position only for SPAN element inside editable div.

My Code:

private getCaretPosition() {
   let caretRevCount = 0;
   if (window.getSelection) {
      const selection = window.getSelection();
      const currentNode = selection.focusNode.parentNode;
      caretRevCount = selection.focusOffset;
      let previousNode = currentNode.previousSibling;
      while(previousNode && previousNode.nodeName === 'SPAN') { 
      // you can check specific element
      caretRevCount += previousNode.textContent.length;
      previousNode = previousNode.previousSibling;
      }
    }
    return caretRevCount;
}

How code works:

Example scenario: "Hi there, this| is sample text".

Caret position: At the end of "this" text.

  1. Initially, getting the selection area where caret is present from window.getSelection() method.
  2. selection.focusOffSet returns only currentNode text length. In Eg. case currentNode is "this". It returns 4 to caretRevCount.
  3. My approach is to backtrack from current node. So, I am looping previous nodes which is ["there, " , "Hi"] and adding its text length to caretRevCount.
  4. Finally, after the loop gets completed caretRevCount returns a sum value which is caretPosition.
夜还是长夜 2024-10-05 08:27:05

如果将可编辑 div 样式设置为“display:inline-block;white-space:pre-wrap”,则在输入新行时不会获得新的子 div,而只会获得 LF 字符(即 ) ;.

function showCursPos(){
    selection = document.getSelection();
    childOffset = selection.focusOffset;
    const range = document.createRange();
    eDiv = document.getElementById("eDiv");
    range.setStart(eDiv, 0);
    range.setEnd(selection.focusNode, childOffset);
    var sHtml = range.toString();
    p = sHtml.length; 
    sHtml=sHtml.replace(/(\r)/gm, "\\r");
    sHtml=sHtml.replace(/(\n)/gm, "\\n");
    document.getElementById("caretPosHtml").value=p;
    document.getElementById("exHtml").value=sHtml;   
  }
click/type in div below:
<br>
<div contenteditable name="eDiv" id="eDiv"  
     onkeyup="showCursPos()" onclick="showCursPos()" 
     style="width: 10em; border: 1px solid; display:inline-block; white-space: pre-wrap; "
     >123
456
789</div>
<p>
html caret position:<br> <input type="text" id="caretPosHtml">
<p>  
html from start of div:<br> <input type="text" id="exHtml">

我注意到,当您在可编辑 div 中按“enter”键时,它会创建一个新节点,因此 focusOffset 重置为零。这就是为什么我必须添加一个范围变量,并将其从子节点的 focusOffset 扩展到 eDiv 的开头(从而捕获其间的所有文本)。

If you set the editable div style to "display:inline-block; white-space: pre-wrap" you don't get new child divs when you enter a new line, you just get LF character (i.e. );.

function showCursPos(){
    selection = document.getSelection();
    childOffset = selection.focusOffset;
    const range = document.createRange();
    eDiv = document.getElementById("eDiv");
    range.setStart(eDiv, 0);
    range.setEnd(selection.focusNode, childOffset);
    var sHtml = range.toString();
    p = sHtml.length; 
    sHtml=sHtml.replace(/(\r)/gm, "\\r");
    sHtml=sHtml.replace(/(\n)/gm, "\\n");
    document.getElementById("caretPosHtml").value=p;
    document.getElementById("exHtml").value=sHtml;   
  }
click/type in div below:
<br>
<div contenteditable name="eDiv" id="eDiv"  
     onkeyup="showCursPos()" onclick="showCursPos()" 
     style="width: 10em; border: 1px solid; display:inline-block; white-space: pre-wrap; "
     >123
456
789</div>
<p>
html caret position:<br> <input type="text" id="caretPosHtml">
<p>  
html from start of div:<br> <input type="text" id="exHtml">

What I noticed was when you press "enter" in the editable div, it creates a new node, so the focusOffset resets to zero. This is why I've had to add a range variable, and extend it from the child nodes' focusOffset back to the start of eDiv (and thus capturing all text in-between).

源来凯始玺欢你 2024-10-05 08:27:05

这个建立在 @alockwood05 的答案之上,并为 contenteditable div 内带有嵌套标签的插入符号以及节点内的偏移量提供获取和设置功能,以便您拥有一个可以通过偏移量进行序列化和反序列化的解决方案。

我在跨平台代码编辑器中使用此解决方案,需要在通过词法分析器/解析器进行语法突出显示之前获取插入符开始/结束位置,然后立即将其设置回来。

function countUntilEndContainer(parent, endNode, offset, countingState = {count: 0}) {
    for (let node of parent.childNodes) {
        if (countingState.done) break;
        if (node === endNode) {
            countingState.done = true;
            countingState.offsetInNode = offset;
            return countingState;
        }
        if (node.nodeType === Node.TEXT_NODE) {
            countingState.offsetInNode = offset;
            countingState.count += node.length;
        } else if (node.nodeType === Node.ELEMENT_NODE) {
            countUntilEndContainer(node, endNode, offset, countingState);
        } else {
            countingState.error = true;
        }
    }
    return countingState;
}

function countUntilOffset(parent, offset, countingState = {count: 0}) {
    for (let node of parent.childNodes) {
        if (countingState.done) break;
        if (node.nodeType === Node.TEXT_NODE) {
            if (countingState.count <= offset && offset < countingState.count + node.length)
            {
                countingState.offsetInNode = offset - countingState.count;
                countingState.node = node; 
                countingState.done = true; 
                return countingState; 
            }
            else { 
                countingState.count += node.length; 
            }
        } else if (node.nodeType === Node.ELEMENT_NODE) {
            countUntilOffset(node, offset, countingState);
        } else {
            countingState.error = true;
        }
    }
    return countingState;
}

function getCaretPosition()
{
    let editor = document.getElementById('editor');
    let sel = window.getSelection();
    if (sel.rangeCount === 0) { return null; }
    let range = sel.getRangeAt(0);    
    let start = countUntilEndContainer(editor, range.startContainer, range.startOffset);
    let end = countUntilEndContainer(editor, range.endContainer, range.endOffset);
    let offsetsCounts = { start: start.count + start.offsetInNode, end: end.count + end.offsetInNode };
    let offsets = { start: start, end: end, offsets: offsetsCounts };
    return offsets;
}

function setCaretPosition(start, end)
{
    let editor = document.getElementById('editor');
    let sel = window.getSelection();
    if (sel.rangeCount === 0) { return null; }
    let range = sel.getRangeAt(0);
    let startNode = countUntilOffset(editor, start);
    let endNode = countUntilOffset(editor, end);
    let newRange = new Range();
    newRange.setStart(startNode.node, startNode.offsetInNode);
    newRange.setEnd(endNode.node, endNode.offsetInNode);
    sel.removeAllRanges();
    sel.addRange(newRange);
    return true;
}

This one builds on @alockwood05's answer and provides both get and set functionality for a caret with nested tags inside the contenteditable div as well as the offsets within nodes so that you have a solution that is both serializable and de-serializable by offsets as well.

I'm using this solution in a cross-platform code editor that needs to get the caret start/end position prior to syntax highlighting via a lexer/parser and then set it back immediately afterward.

function countUntilEndContainer(parent, endNode, offset, countingState = {count: 0}) {
    for (let node of parent.childNodes) {
        if (countingState.done) break;
        if (node === endNode) {
            countingState.done = true;
            countingState.offsetInNode = offset;
            return countingState;
        }
        if (node.nodeType === Node.TEXT_NODE) {
            countingState.offsetInNode = offset;
            countingState.count += node.length;
        } else if (node.nodeType === Node.ELEMENT_NODE) {
            countUntilEndContainer(node, endNode, offset, countingState);
        } else {
            countingState.error = true;
        }
    }
    return countingState;
}

function countUntilOffset(parent, offset, countingState = {count: 0}) {
    for (let node of parent.childNodes) {
        if (countingState.done) break;
        if (node.nodeType === Node.TEXT_NODE) {
            if (countingState.count <= offset && offset < countingState.count + node.length)
            {
                countingState.offsetInNode = offset - countingState.count;
                countingState.node = node; 
                countingState.done = true; 
                return countingState; 
            }
            else { 
                countingState.count += node.length; 
            }
        } else if (node.nodeType === Node.ELEMENT_NODE) {
            countUntilOffset(node, offset, countingState);
        } else {
            countingState.error = true;
        }
    }
    return countingState;
}

function getCaretPosition()
{
    let editor = document.getElementById('editor');
    let sel = window.getSelection();
    if (sel.rangeCount === 0) { return null; }
    let range = sel.getRangeAt(0);    
    let start = countUntilEndContainer(editor, range.startContainer, range.startOffset);
    let end = countUntilEndContainer(editor, range.endContainer, range.endOffset);
    let offsetsCounts = { start: start.count + start.offsetInNode, end: end.count + end.offsetInNode };
    let offsets = { start: start, end: end, offsets: offsetsCounts };
    return offsets;
}

function setCaretPosition(start, end)
{
    let editor = document.getElementById('editor');
    let sel = window.getSelection();
    if (sel.rangeCount === 0) { return null; }
    let range = sel.getRangeAt(0);
    let startNode = countUntilOffset(editor, start);
    let endNode = countUntilOffset(editor, end);
    let newRange = new Range();
    newRange.setStart(startNode.node, startNode.offsetInNode);
    newRange.setEnd(endNode.node, endNode.offsetInNode);
    sel.removeAllRanges();
    sel.addRange(newRange);
    return true;
}
小…红帽 2024-10-05 08:27:05

我使用了 John Ernest 的优秀代码,并根据我的需要对其进行了一些修改:

  • 使用 TypeScript(在 Angular 应用程序中) ;
  • 使用略有不同的数据结构。

在研究它的过程中,我偶然发现了鲜为人知(或很少使用)的 TreeWalker,并进一步简化了代码,因为它允许摆脱递归性。

一种可能的优化可能是遍历树一次以找到起始节点和结束节点,但是:

  • 我怀疑用户是否能感觉到速度增益,即使是在一个巨大而复杂的页面的末尾;
  • 这将使算法更加复杂且可读性较差。

相反,我处理了开头与结尾相同的情况(只是一个插入符号,没有真正的选择)。

[编辑] 范围的节点似乎始终是文本类型,因此我进一步简化了代码,并且它允许在不进行转换的情况下获取节点长度。

这是代码:

export type CountingState = {
    countBeforeNode: number;
    offsetInNode: number;
    node?: Text; // Always of Text type
};

export type RangeOffsets = {
    start: CountingState;
    end: CountingState;
    offsets: { start: number; end: number; }
};

export function isTextNode(node: Node): node is Text {
    return node.nodeType === Node.TEXT_NODE;
}

export function getCaretPosition(container: Node): RangeOffsets | undefined {
    const selection = window.getSelection();
    if (!selection || selection.rangeCount === 0) { return undefined; }
    const range = selection.getRangeAt(0);
    const start = countUntilEndNode(container, range.startContainer as Text, range.startOffset);
    const end = range.collapsed ? start : countUntilEndNode(container, range.endContainer as Text, range.endOffset);
    const offsets = { start: start.countBeforeNode + start.offsetInNode, end: end.countBeforeNode + end.offsetInNode };
    const rangeOffsets: RangeOffsets = { start, end, offsets };
    return rangeOffsets;
}

export function setCaretPosition(container: Node, start: number, end: number): boolean {
    const selection = window.getSelection();
    if (!selection) { return false; }
    const startState = countUntilOffset(container, start);
    const endState = start === end ? startState : countUntilOffset(container, end);
    const range = document.createRange(); // new Range() doesn't work for me!
    range.setStart(startState.node!, startState.offsetInNode);
    range.setEnd(endState.node!, endState.offsetInNode);
    selection.removeAllRanges();
    selection.addRange(range);
    return true;
}

function countUntilEndNode(
    parent: Node,
    endNode: Text,
    offset: number,
    countingState: CountingState = { countBeforeNode: 0, offsetInNode: 0 },
): CountingState {
    const treeWalker = document.createTreeWalker(parent, NodeFilter.SHOW_TEXT);
    while (treeWalker.nextNode()) {
        const node = treeWalker.currentNode as Text;
        if (node === endNode) {
            // We found the target node, memorize it.
            countingState.node = node;
            countingState.offsetInNode = offset;
            break;
        }
        // Add length of text nodes found in the way, until we find the target node.
        countingState.countBeforeNode += node.length;
    }
    return countingState;
}

function countUntilOffset(
    parent: Node,
    offset: number,
    countingState: CountingState = { countBeforeNode: 0, offsetInNode: 0 },
): CountingState {
    const treeWalker = document.createTreeWalker(parent, NodeFilter.SHOW_TEXT);
    while (treeWalker.nextNode()) {
        const node = treeWalker.currentNode as Text;
        if (countingState.countBeforeNode <= offset && offset < countingState.countBeforeNode + node.length) {
            countingState.offsetInNode = offset - countingState.countBeforeNode;
            countingState.node = node;
            break;
        }
        countingState.countBeforeNode += node.length;
    }
    return countingState;
}

I used John Ernest's excellent code, and reworked it a bit for my needs:

  • Using TypeScript (in an Angular application);
  • Using a slightly different data structure.

And while working on it, I stumbled on the little known (or little used) TreeWalker, and simplified the code further, as it allows to get rid of recursivity.

A possible optimization could be to walk the tree once to find both start node and end node, but:

  • I doubt the speed gain would be perceptible by the user, even at the end of a huge, complex page;
  • It would make the algorithm more complex and less readable.

Instead, I treated the case where the start is the same as the end (just a caret, no real selection).

[EDIT] It seems that range's nodes are always of Text type, so I simplified code a bit more, and it allows to get the node length without casting it.

Here is the code:

export type CountingState = {
    countBeforeNode: number;
    offsetInNode: number;
    node?: Text; // Always of Text type
};

export type RangeOffsets = {
    start: CountingState;
    end: CountingState;
    offsets: { start: number; end: number; }
};

export function isTextNode(node: Node): node is Text {
    return node.nodeType === Node.TEXT_NODE;
}

export function getCaretPosition(container: Node): RangeOffsets | undefined {
    const selection = window.getSelection();
    if (!selection || selection.rangeCount === 0) { return undefined; }
    const range = selection.getRangeAt(0);
    const start = countUntilEndNode(container, range.startContainer as Text, range.startOffset);
    const end = range.collapsed ? start : countUntilEndNode(container, range.endContainer as Text, range.endOffset);
    const offsets = { start: start.countBeforeNode + start.offsetInNode, end: end.countBeforeNode + end.offsetInNode };
    const rangeOffsets: RangeOffsets = { start, end, offsets };
    return rangeOffsets;
}

export function setCaretPosition(container: Node, start: number, end: number): boolean {
    const selection = window.getSelection();
    if (!selection) { return false; }
    const startState = countUntilOffset(container, start);
    const endState = start === end ? startState : countUntilOffset(container, end);
    const range = document.createRange(); // new Range() doesn't work for me!
    range.setStart(startState.node!, startState.offsetInNode);
    range.setEnd(endState.node!, endState.offsetInNode);
    selection.removeAllRanges();
    selection.addRange(range);
    return true;
}

function countUntilEndNode(
    parent: Node,
    endNode: Text,
    offset: number,
    countingState: CountingState = { countBeforeNode: 0, offsetInNode: 0 },
): CountingState {
    const treeWalker = document.createTreeWalker(parent, NodeFilter.SHOW_TEXT);
    while (treeWalker.nextNode()) {
        const node = treeWalker.currentNode as Text;
        if (node === endNode) {
            // We found the target node, memorize it.
            countingState.node = node;
            countingState.offsetInNode = offset;
            break;
        }
        // Add length of text nodes found in the way, until we find the target node.
        countingState.countBeforeNode += node.length;
    }
    return countingState;
}

function countUntilOffset(
    parent: Node,
    offset: number,
    countingState: CountingState = { countBeforeNode: 0, offsetInNode: 0 },
): CountingState {
    const treeWalker = document.createTreeWalker(parent, NodeFilter.SHOW_TEXT);
    while (treeWalker.nextNode()) {
        const node = treeWalker.currentNode as Text;
        if (countingState.countBeforeNode <= offset && offset < countingState.countBeforeNode + node.length) {
            countingState.offsetInNode = offset - countingState.countBeforeNode;
            countingState.node = node;
            break;
        }
        countingState.countBeforeNode += node.length;
    }
    return countingState;
}
岁月打碎记忆 2024-10-05 08:27:05

一种直接的方法,迭代 contenteditable div 的所有子级,直到到达 endContainer。然后我添加结束容器偏移量,我们就得到了字符索引。应该适用于任意数量的嵌套。使用递归。

注意:对于 ie 需要 poly fill支持 Element.closest('div[contenteditable]')

https:// codepen.io/alockwood05/pen/vMpdmZ

function caretPositionIndex() {
    const range = window.getSelection().getRangeAt(0);
    const { endContainer, endOffset } = range;

    // get contenteditableDiv from our endContainer node
    let contenteditableDiv;
    const contenteditableSelector = "div[contenteditable]";
    switch (endContainer.nodeType) {
      case Node.TEXT_NODE:
        contenteditableDiv = endContainer.parentElement.closest(contenteditableSelector);
        break;
      case Node.ELEMENT_NODE:
        contenteditableDiv = endContainer.closest(contenteditableSelector);
        break;
    }
    if (!contenteditableDiv) return '';


    const countBeforeEnd = countUntilEndContainer(contenteditableDiv, endContainer);
    if (countBeforeEnd.error ) return null;
    return countBeforeEnd.count + endOffset;

    function countUntilEndContainer(parent, endNode, countingState = {count: 0}) {
      for (let node of parent.childNodes) {
        if (countingState.done) break;
        if (node === endNode) {
          countingState.done = true;
          return countingState;
        }
        if (node.nodeType === Node.TEXT_NODE) {
          countingState.count += node.length;
        } else if (node.nodeType === Node.ELEMENT_NODE) {
          countUntilEndContainer(node, endNode, countingState);
        } else {
          countingState.error = true;
        }
      }
      return countingState;
    }
  }

A straight forward way, that iterates through all the chidren of the contenteditable div until it hits the endContainer. Then I add the end container offset and we have the character index. Should work with any number of nestings. uses recursion.

Note: requires a poly fill for ie to support Element.closest('div[contenteditable]')

https://codepen.io/alockwood05/pen/vMpdmZ

function caretPositionIndex() {
    const range = window.getSelection().getRangeAt(0);
    const { endContainer, endOffset } = range;

    // get contenteditableDiv from our endContainer node
    let contenteditableDiv;
    const contenteditableSelector = "div[contenteditable]";
    switch (endContainer.nodeType) {
      case Node.TEXT_NODE:
        contenteditableDiv = endContainer.parentElement.closest(contenteditableSelector);
        break;
      case Node.ELEMENT_NODE:
        contenteditableDiv = endContainer.closest(contenteditableSelector);
        break;
    }
    if (!contenteditableDiv) return '';


    const countBeforeEnd = countUntilEndContainer(contenteditableDiv, endContainer);
    if (countBeforeEnd.error ) return null;
    return countBeforeEnd.count + endOffset;

    function countUntilEndContainer(parent, endNode, countingState = {count: 0}) {
      for (let node of parent.childNodes) {
        if (countingState.done) break;
        if (node === endNode) {
          countingState.done = true;
          return countingState;
        }
        if (node.nodeType === Node.TEXT_NODE) {
          countingState.count += node.length;
        } else if (node.nodeType === Node.ELEMENT_NODE) {
          countUntilEndContainer(node, endNode, countingState);
        } else {
          countingState.error = true;
        }
      }
      return countingState;
    }
  }
む无字情书 2024-10-05 08:27:05

该答案适用于使用递归函数的嵌套文本元素。

This answer works with nested text elements, using recursive functions. ????

Bonus: sets the caret position to saved position.

function getCaretData(elem) {
  var sel = window.getSelection();
  return [sel.anchorNode, sel.anchorOffset];
}

function setCaret(el, pos) {
  var range = document.createRange();
  var sel = window.getSelection();
  range.setStart(el,pos);
  range.collapse(true);
  sel.removeAllRanges();
  sel.addRange(range);
}


let indexStack = [];

function checkParent(elem) {
  
  let parent = elem.parentNode;
  let parentChildren = Array.from(parent.childNodes);
  
  let elemIndex = parentChildren.indexOf(elem);
  
  indexStack.unshift(elemIndex);
  
  if (parent !== cd) {
    
    checkParent(parent);
    
  } else {
    
    return;
    
  }
  
}

let stackPos = 0;
let elemToSelect;

function getChild(parent, index) {
  
  let child = parent.childNodes[index];
  
  if (stackPos < indexStack.length-1) {
    
    stackPos++;
        
    getChild(child, indexStack[stackPos]);
    
  } else {
    
    elemToSelect = child;
    
    return;
    
  }
  
}


let cd = document.querySelector('.cd'),
    caretpos = document.querySelector('.caretpos');

cd.addEventListener('keyup', () => {
  
  let caretData = getCaretData(cd);
  
  let selectedElem = caretData[0];
  let caretPos = caretData[1];
  
  
  indexStack = [];
  checkParent(selectedElem);
    
  
  cd.innerHTML = 'Hello world! <span>Inline! <span>In inline!</span></span>';
  
  
  stackPos = 0;
  getChild(cd, indexStack[stackPos]);
  
  
  setCaret(elemToSelect, caretPos);
  
  
  caretpos.innerText = 'indexStack: ' + indexStack + '. Got child: ' + elemToSelect.data + '. Moved caret to child at pos: ' + caretPos;
  
})
.cd, .caretpos {
  font-family: system-ui, Segoe UI, sans-serif;
  padding: 10px;
}

.cd span {
  display: inline-block;
  color: purple;
  padding: 5px;
}

.cd span span {
  color: chocolate;
  padding: 3px;
}

:is(.cd, .cd span):hover {
  border-radius: 3px;
  box-shadow: inset 0 0 0 2px #005ecc;
}
<div class="cd" contenteditable="true">Hello world! <span>Inline! <span>In inline!</span></span></div>
<div class="caretpos">Move your caret inside the elements above ⤴</div>

Codepen

不弃不离 2024-10-05 08:27:05

因此,根据 Chris Sullivan 提供的答案,我设法创建了一个版本,当通过键盘进行选择时,它不会重置,并且能够检测列号和行号。

在此方法中,您首先必须找出一种解决方案来获取最多克拉的所有文本。您可以通过获取当前选择(即插入符号)、克隆其第一个范围、折叠它,然后将范围的起始节点更改为元素的开头来完成此操作。从那里,您只需在范围上运行 toString 即可提取最多克拉的所有文本。现在您已经有了文本,我们可以对其执行一些简单的计算以确定行号和列。

对于行号,您只需计算文本字符串中换行符的数量。这可以使用一些简单的正则表达式来完成,可以在下面的代码中看到。

对于列号,可以通过三种方式获取“列号”。

  1. 行号的“相对列”,类似于 Windows 记事本的计算方式,是最容易计算的。这只是范围的结束偏移量 (range.endOffset)。
  2. 插入符的实际位置相对于您需要按下箭头键才能到达该位置的次数。这可以通过替换文本中的所有换行符然后获取其长度来计算。
  3. 插入符相对于实际文本的实际位置;您只需获取文本的长度即可获取此信息。

说得够多了,现在是时候展示一下了:

// Caret
function getCaretPosition(element) {
    // Check for selection
    if (window.getSelection().type == "None") {
        return {
            "ln": -1,
            "col": -1
        }
    }

    // Copy range
    var selection = window.getSelection();
    var range = selection.getRangeAt(0).cloneRange();

    // Collapse range
    range.collapse();

    // Move range to encompass everything
    range.setStart(element.firstChild, 0);

    // Calculate position
    var content = range.toString();
    var text = JSON.stringify(content);
    var lines = (text.match(/\\n/g) || []).length + 1;

    // Return caret position (col - 2 due to some weird calculation with regex)
    return {
        "ln": lines,
        // "col": range.endOffset + 1 // Method 1
        "col": text.replace(/\\n/g, " ").length - 2 // Method 2
        // "col": text.length -2 // Method 3
    }
}

现在,通过这种方法,如果您愿意,您可以在每次更新选择时获得插入符位置:

document.addEventListener("selectionchange", function(e) {
    console.log(getCaretPosition(document.getElementById("text-area")));
});

我希望这对某人有帮助,我花了几个小时拔出头发试图弄清楚如何这样做!

So based off of the answer provided by Chris Sullivan, I managed to create a version of it that wouldn't reset when a selection was made via keyboard and was able to detect both the column and the line number.

In this method, you first have to figure out a solution to fetching all of the text up to the carat. You can do this by getting the current selection (which is the caret), cloning the first range of it, collapsing it, then changing the start node of the range to be the beginning of your element. From there, you can extract all the text up to the carat by simply running a toString on the range. Now that you have the text, we can perform some simple calculations on it to determine the line number and column.

For the line number, you simply need to calculate the number of newlines in the string of text. This can be done using some simple regexp, which can be seen in the code below.

For the column number, there's three ways to get a "column number."

  1. The "relative column" to the line number, similar to how Windows Notepad calculates it, is the easiest to calculate. This is simply the range's end offset (range.endOffset).
  2. The actual position of the caret relative to the number of arrow-key presses you would need to press to get to that position. This can be calculated by replacing all of the newlines in the text, and then getting the length of it.
  3. The actual position of the caret relative to the actual text; this you can fetch by just getting the length of the text.

Enough talk, now time for some show:

// Caret
function getCaretPosition(element) {
    // Check for selection
    if (window.getSelection().type == "None") {
        return {
            "ln": -1,
            "col": -1
        }
    }

    // Copy range
    var selection = window.getSelection();
    var range = selection.getRangeAt(0).cloneRange();

    // Collapse range
    range.collapse();

    // Move range to encompass everything
    range.setStart(element.firstChild, 0);

    // Calculate position
    var content = range.toString();
    var text = JSON.stringify(content);
    var lines = (text.match(/\\n/g) || []).length + 1;

    // Return caret position (col - 2 due to some weird calculation with regex)
    return {
        "ln": lines,
        // "col": range.endOffset + 1 // Method 1
        "col": text.replace(/\\n/g, " ").length - 2 // Method 2
        // "col": text.length -2 // Method 3
    }
}

Now through this method, if you wanted, you can get the caret position every time the selection is updated:

document.addEventListener("selectionchange", function(e) {
    console.log(getCaretPosition(document.getElementById("text-area")));
});

I hope this helps someone, I was pulling my hair out for hours trying to figure out how to do this!

寂寞笑我太脆弱 2024-10-05 08:27:05

获取插入符相对于可编辑内容的索引位置:

// Get a reference to the content editable element
const divRef = querySelector('#contentBox');

const getCaretPosition = () => {
  var selection = document.getSelection();
  if (!selection || !divRef) return 0;
  selection.collapseToEnd();
  const range = selection.getRangeAt(0);
  const clone = range.cloneRange();
  clone.selectNodeContents(divRef);
  clone.setEnd(range.startContainer, range.startOffset);
  return clone.toString().length;
}

Get the caret's index position relative to the content editable:

// Get a reference to the content editable element
const divRef = querySelector('#contentBox');

const getCaretPosition = () => {
  var selection = document.getSelection();
  if (!selection || !divRef) return 0;
  selection.collapseToEnd();
  const range = selection.getRangeAt(0);
  const clone = range.cloneRange();
  clone.selectNodeContents(divRef);
  clone.setEnd(range.startContainer, range.startOffset);
  return clone.toString().length;
}
故人爱我别走 2024-10-05 08:27:05

下面的代码通过获取当前元素的偏移量,然后导航回 contenteditable 内的所有元素并计算字符总数来计算插入符位置。

这将:

  • 不会破坏格式化功能
  • 处理多行。

如果您遇到问题,请告诉我,以便我更新代码。

function getRowTextLength(currentNode) {
    let previousSibling;
    let textLength = 0;
    //this means we are outside our desired scope
    if (currentNode?.contentEditable == "true") {
        return textLength;
    }
    while (currentNode) {
        //get the previous element of the currentNode
        previousSibling =
            currentNode.previousSibling || //UNFORMATTED text case
            //avoid targetting the contenteditable div itself
            (currentNode.parentNode.nodeName != "DIV"
                ? currentNode.parentNode.previousSibling //FORMATTED text case
                : null);

        //count the number of characters in the previous element, if exists
        textLength = previousSibling
            ? textLength + previousSibling.textContent.length
            : textLength;
        //set current element as previous element
        currentNode = previousSibling;
        //continue looping as long as we have a previous element
    }
    return textLength;
}

//pass e.target from an eventListener as argument
function getCaretPosition(element) {
    let selection = getSelection(element);
    //caret position at current row
    let caretPosition = selection.anchorOffset;
    let currentNode = selection.baseNode;

    caretPosition += getRowTextLength(currentNode);

    //get closest div parent node
    if (caretPosition != 0) {
        do {
            currentNode = currentNode.parentNode;
        } while (currentNode.nodeName != "DIV");
    }

    caretPosition += getRowTextLength(currentNode);

    //console.log("CARET POSITION ", caretPosition);
    return caretPosition;
}

The code below counts the caret position by taking the offset at the current element and then navigating back all the elements inside the contenteditable and counting the total number of characters.

This will:

  • Not break formatting functionality
  • Work with multiple rows.

If you encounter an issue please let me know so I can update the code.

function getRowTextLength(currentNode) {
    let previousSibling;
    let textLength = 0;
    //this means we are outside our desired scope
    if (currentNode?.contentEditable == "true") {
        return textLength;
    }
    while (currentNode) {
        //get the previous element of the currentNode
        previousSibling =
            currentNode.previousSibling || //UNFORMATTED text case
            //avoid targetting the contenteditable div itself
            (currentNode.parentNode.nodeName != "DIV"
                ? currentNode.parentNode.previousSibling //FORMATTED text case
                : null);

        //count the number of characters in the previous element, if exists
        textLength = previousSibling
            ? textLength + previousSibling.textContent.length
            : textLength;
        //set current element as previous element
        currentNode = previousSibling;
        //continue looping as long as we have a previous element
    }
    return textLength;
}

//pass e.target from an eventListener as argument
function getCaretPosition(element) {
    let selection = getSelection(element);
    //caret position at current row
    let caretPosition = selection.anchorOffset;
    let currentNode = selection.baseNode;

    caretPosition += getRowTextLength(currentNode);

    //get closest div parent node
    if (caretPosition != 0) {
        do {
            currentNode = currentNode.parentNode;
        } while (currentNode.nodeName != "DIV");
    }

    caretPosition += getRowTextLength(currentNode);

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