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

Posted

技术标签:

【中文标题】获取内容可编辑的插入符号位置【英文标题】:Get contentEditable caret position 【发布时间】:2011-04-27 16:27:59 【问题描述】:

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

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

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

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

【问题讨论】:

查看它在文本中的位置。然后,查找该位置之前最后出现的“@”。所以只是一些文本逻辑。 另外,我不打算在 中允许其他标签,只有文本 好的,是的,我 am 需要 中的其他标签。会有标签,但不会有嵌套... @Bertvan:如果插入符号在 &lt;a&gt; 元素内 &lt;div&gt; 内,那么你想要什么偏移量? &lt;a&gt;?内文本内的偏移量? 它不应该在 元素内。 元素应该呈现为 html,因此用户实际上不能将插入符号放在那里。 【参考方案1】:

以下代码假设:

可编辑的&lt;div&gt; 中始终只有一个文本节点,没有其他节点 可编辑的 div 没有将 CSS white-space 属性设置为 pre

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

https://***.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>

【讨论】:

如果里面有任何其他标签,这将不起作用。问题:如果插入符号位于 &lt;div&gt; 内的 &lt;a&gt; 元素内,那么您想要什么偏移量? &lt;a&gt;?内文本内的偏移量? @Richard:好吧,keyup 可能是错误的事件,但它是原始问题中使用的事件。 getCaretPosition() 本身在它自己的限制范围内是好的。 如果我按下回车并换行,JSFIDDLE 演示会失败。该位置将显示 0。 @giorgio79:是的,因为换行符会生成&lt;br&gt;&lt;div&gt; 元素,这违反了答案中提到的第一个假设。如果您需要更通用的解决方案,可以尝试***.com/a/4812022/96100 有没有办法这样做,所以它包括行号?【参考方案2】:

我认为其他答案没有解决的一些问题:

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

这是一种获取开始和结束位置作为元素 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]];

【讨论】:

这个必须选为正确答案。它适用于文本中的标签(接受的响应不适用) 有没有办法包含换行符?按“enter”不会改变这个函数的结果。我也知道问题中没有提到它,但是等效的“setCaretPosition”会非常有帮助 重新换行:是的,但它是一个更复杂的解决方案。换行符在文本节点中表示为插入到节点树中的无文本 BR 节点,这些节点未正确反映在 textContent 中。因此,要处理它们,基本上任何对 textContent 的引用都必须替换为一个函数,例如“getNodeInnerText()”将遍历节点树并构造正确的文本字符串,特别是将为任何 BR 节点插入“\n”(在大多数情况下——它比这更微妙) setCaretPosition 在这里被询问/回答:***.com/questions/512528/…(虽然我使用的是解决方案的修改版本,但不记得为什么) @WeAreDoomed 请参阅上述评论re setCaretPosition【参考方案3】:

$("#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>

【讨论】:

不幸的是,一旦您按 Enter 并从另一行开始(它再次从 0 开始 - 可能从 CR/LF 开始计数),这将停止工作。 如果你有一些粗体和/或斜体字,它就不能正常工作。【参考方案4】:

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

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 )
&lt;div contenteditable&gt;some text here &lt;i&gt;italic text here&lt;/i&gt; some other text here &lt;b&gt;bold text here&lt;/b&gt; end of text&lt;/div&gt;

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

【讨论】:

如果我有&lt;div contenteditable&gt; some text here &lt;i&gt;italic text here&lt;/i&gt; some other text here &lt;b&gt;bold text here&lt;/b&gt; end of text &lt;/div&gt; 每次我将光标放在i 标记或div 内的任何子html 元素之前,光标位置从0 开始。有没有办法逃避这个重新启动计数? 奇数。我在 Chrome 中没有这种行为。你用的是什么浏览器? 看起来 selection.modify 可能在所有浏览器上都支持,也可能不支持。 developer.mozilla.org/en-US/docs/Web/API/Selection 非常好。干得好。 在 Firefox 中不工作:/ NS_ERROR_NOT_IMPLEMENTED selection.modify 看起来此浏览器不支持它:developer.mozilla.org/en-US/docs/Web/API/Selection/modify【参考方案5】:

试试这个:

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

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

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

【讨论】:

这很甜蜜。当单击按钮重命名li 的内容时,我需要这种行为来将插入符号设置为contenteditable li 的结尾。 @androidDev 我不是 Caret.js 的作者,但您是否认为获得所有主流浏览器的插入符号位置比几行代码更复杂? 您知道或已经创建了可以与我们分享的非臃肿替代方案吗?【参考方案6】:

window.getSelection - vs - 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 )
&lt;div contenteditable&gt;some text here &lt;i&gt;italic text here&lt;/i&gt; some other text here &lt;b&gt;bold text here&lt;/b&gt; end of text&lt;/div&gt;

调用线路取决于事件类型,关键事件使用这个:

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

鼠标事件使用这个:

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

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

【讨论】:

唯一适用于新行的解决方案,谢谢 在 Firefox 和 chrome 中都可以使用,谢谢【参考方案7】:
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
    ;

【讨论】:

这个确实对我有用,上面的方法我都试过了,都没有。 谢谢,但它也会在新行返回 x: 0, y: 0。 返回像素位置,而不是字符偏移量 谢谢,我正在寻找从插入符号检索像素位置,它工作正常。 谢谢!这就是我一直在寻找的,坐标而不是字符偏移似乎可以工作!【参考方案8】:

因为我花了很长时间才弄清楚如何使用新的window.getSelection 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,在keyup 上触发。但是请注意,快速方向键按下以及快速删除似乎是跳过事件。

【讨论】:

为我工作!非常感谢。 使用此文本选择不再可能,因为它已折叠。可能的场景:需要评估每个 keyUp 事件 对我来说很好用,谢谢【参考方案9】:
//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)

【讨论】:

谢谢,但我究竟如何获得 div 内容中插入符号位置的“索引”? 好的,看起来在 .getSelection() 上调用 .baseOffset 就可以了。因此,这与您的回答一起回答了我的问题。谢谢! 不幸的是 .baseOffset 仅适用于 webkit(我认为)。它也只给你从插入符号的直接​​父级的偏移量(如果你在 中有一个 标签,它将给出从 开始的偏移量,而不是 的开始. 基于标准的范围可以使用 range.endOffset range.startOffset range.endContainer 和 range.startContainer 来获取选择的父 node 的偏移量,以及节点本身(这包括文本节点)。提供 range.offsetLeft,它是 pixels 中从左侧的偏移量,所以没用。 最好只存储范围对象本身并使用 window.getSelection().addrange(range); 大多数浏览器返回的Range对象和IE返回的TextRange对象是截然不同的东西,所以我不确定这个答案是否能解决很多问题。 【参考方案10】:

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

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&#13;&#10;456&#10;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 的开头(从而捕获中间的所有文本)。

【讨论】:

这在 Chrome 和 Firefox 中非常适合我【参考方案11】:

这个建立在@alockwood05 的答案之上,并为插入符提供了 get 和 set 功能,其中包含 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;

【讨论】:

【参考方案12】:

这个适用于角度

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;

【讨论】:

欢迎来到 Stack Overflow。亲爱的@Parthybaraja V,请详细回答问题。 它有效,只是一个问题,您如何首先获得 Div 文本?我在 div 上使用 [(ngModel)],但它是空的 @Amirreza 谢谢!我在 div 中使用 id 属性并使用 document.getElementById 函数获取值【参考方案13】:

一种直接的方式,遍历 contenteditable div 的所有子项,直到它到达 endContainer。然后我添加结束容器偏移量,我们就有了字符索引。应该与任意数量的嵌套一起使用。使用递归。

注意:需要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;
    
  

【讨论】:

【参考方案14】:

此答案适用于嵌套文本元素,使用递归函数。 ?

奖励:将插入符号位置设置为已保存位置。

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

【讨论】:

【参考方案15】:

我使用了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;

【讨论】:

以上是关于获取内容可编辑的插入符号位置的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Angular 2 中获取内容可编辑的“div”的插入符号位置?

有没有办法阻止内容可编辑的插入符号出现在 IE 10 中的元素上?

通过 javascript 在 contenteditable div 上获取插入符号坐标。

如何使用 html 子元素在 contenteditable div 中获取插入符号位置?

将插入符号位置设置为零在 chrome 中的 contenteditable 元素的节点内

在插入符号位置插入HTML而不创建对象