如何在 contenteditable 元素 (div) 中设置插入符号 (光标) 位置?

IT技术 javascript jquery contenteditable caret cursor-position
2021-02-06 13:35:18

我以这个简单的 HTML 为例:

<div id="editable" contenteditable="true">
  text text text<br>
  text text text<br>
  text text text<br>
</div>
<button id="button">focus</button>

我想要简单的事情 - 当我单击按钮时,我想将插入符号(光标)放置到可编辑 div 中的特定位置。通过网络搜索,我将此 JS 附加到按钮单击,但它不起作用(FF,Chrome):

var range = document.createRange();
var myDiv = document.getElementById("editable");
range.setStart(myDiv, 5);
range.setEnd(myDiv, 5);

是否可以像这样手动设置插入符号位置?

6个回答

在大多数浏览器中,您需要RangeSelection对象。您将每个选择边界指定为一个节点和该节点内的偏移量。例如,要将插入符号设置为第二行文本的第五个字符,您需要执行以下操作:

function setCaret() {
    var el = document.getElementById("editable")
    var range = document.createRange()
    var sel = window.getSelection()
    
    range.setStart(el.childNodes[2], 5)
    range.collapse(true)
    
    sel.removeAllRanges()
    sel.addRange(range)
}
<div id="editable" contenteditable="true">
  text text text<br>text text text<br>text text text<br>
</div>

<button id="button" onclick="setCaret()">focus</button>

IE < 9 的工作方式完全不同。如果您需要支持这些浏览器,则需要不同的代码。

jsFiddle 示例:http : //jsfiddle.net/timdown/vXnCM/

如何在 span 标签中设置插入符号,如下所示:<<div id="editable" contenteditable="true"> test1<br>test2<br><span></span> </div>
2021-03-11 13:35:18
@Frodik:您可以使用setSelectionRange()从我这里写的答案功能:stackoverflow.com/questions/6240139/...正如我在答案中指出的那样,它无法正确/一致地处理各种事情,但它可能已经足够好了。
2021-03-19 13:35:18
您的解决方案完美无缺。非常感谢。是否有可能使其在“文本上下文”中工作 - 这意味着位置 #5 将是屏幕上的第五个字母,而不是代码中的第五个字母?
2021-04-01 13:35:18
@MalcolmOcean:Barf,因为 IE < 9 没有document.createRange(或window.getSelection,但它不会那么远)。
2021-04-03 13:35:18
@undroid:jsfiddle 在 Mac 上的 Firefox 38.0.5 中对我来说很好用。
2021-04-09 13:35:18

您在 contenteditable 光标定位上找到的大多数答案都相当简单,因为它们仅适用于带有纯文本的输入。一旦您在容器中使用 html 元素,输入的文本就会被拆分为节点并在树结构中自由分布。

为了设置光标位置,我有这个函数,它循环提供节点内的所有子文本节点,并设置从初始节点开始到chars.count字符的范围:

function createRange(node, chars, range) {
    if (!range) {
        range = document.createRange()
        range.selectNode(node);
        range.setStart(node, 0);
    }

    if (chars.count === 0) {
        range.setEnd(node, chars.count);
    } else if (node && chars.count >0) {
        if (node.nodeType === Node.TEXT_NODE) {
            if (node.textContent.length < chars.count) {
                chars.count -= node.textContent.length;
            } else {
                range.setEnd(node, chars.count);
                chars.count = 0;
            }
        } else {
           for (var lp = 0; lp < node.childNodes.length; lp++) {
                range = createRange(node.childNodes[lp], chars, range);

                if (chars.count === 0) {
                    break;
                }
            }
        }
    } 

    return range;
};

然后我用这个函数调用例程:

function setCurrentCursorPosition(chars) {
    if (chars >= 0) {
        var selection = window.getSelection();

        range = createRange(document.getElementById("test").parentNode, { count: chars });

        if (range) {
            range.collapse(false);
            selection.removeAllRanges();
            selection.addRange(range);
        }
    }
};

range.collapse(false) 将光标设置到范围的末尾。我已经用最新版本的 Chrome、IE、Mozilla 和 Opera 对其进行了测试,它们都运行良好。

附注。如果有人感兴趣,我可以使用以下代码获取当前光标位置:

function isChildOf(node, parentId) {
    while (node !== null) {
        if (node.id === parentId) {
            return true;
        }
        node = node.parentNode;
    }

    return false;
};

function getCurrentCursorPosition(parentId) {
    var selection = window.getSelection(),
        charCount = -1,
        node;

    if (selection.focusNode) {
        if (isChildOf(selection.focusNode, parentId)) {
            node = selection.focusNode; 
            charCount = selection.focusOffset;

            while (node) {
                if (node.id === parentId) {
                    break;
                }

                if (node.previousSibling) {
                    node = node.previousSibling;
                    charCount += node.textContent.length;
                } else {
                     node = node.parentNode;
                     if (node === null) {
                         break
                     }
                }
           }
      }
   }

    return charCount;
};

该代码执行与 set 函数相反的操作 - 它获取当前 window.getSelection().focusNode 和 focusOffset 并向后计数遇到的所有文本字符,直到它遇到具有 containerId id 的父节点。isChildOf 函数只是在运行之前检查所提供的节点实际上是所提供的parentId的子节点

该代码应直接工作,没有变化,但我刚刚从插件我已经开发等等都出来砍死一对夫妇一个jQuery采取它这个的-让我知道如果有什么不工作!

在我的待办事项清单上写一个与我的 UI 代码完全分开的代码 - 我会在有时间的时候发布它。
2021-03-21 13:35:18
@Bendihossan - contenteditable div 中的 html 元素被分解为一个树结构,每个 html 元素都有一个节点。getCurrentCursorPosition 获取当前选择位置并返回树计算有多少纯文本字符。Node.id 是 html 元素 id,而 parentId 是指它应该停止计数的 html 元素 id
2021-03-30 13:35:18
为了能够快速测试您的不同解决方案,您能否将您的答案编辑为可运行的代码片段?先感谢您。
2021-03-30 13:35:18
@Bendihossan - 试试这个jsfiddle.net/nrx9yvw9/5 - 出于某种原因,这个例子中的内容可编辑 div 在文本的开头添加了一些字符和回车(它甚至可能是 jsfiddle 本身这样做,因为它没有;t 在我的 asp.net 服务器上做同样的事情)。
2021-04-03 13:35:18
你能提供这个工作的jsfiddle吗?我正在努力弄清楚这是如何工作的,因为我不确定是什么node.id以及parentId与没有示例相关的内容。谢谢 :)
2021-04-08 13:35:18

我重构了@Liam 的回答。我把它放在一个带有静态方法的类中,我让它的函数接收一个元素而不是#id,以及其他一些小的调整。

此代码特别适用于在您可能使用的富文本框中固定光标<div contenteditable="true">在到达以下代码之前,我坚持了几天。

编辑:他的答案和这个答案有一个涉及按回车键的错误。由于 enter 不算作字符,因此在按 Enter 后光标位置会变得混乱。如果我能够修复代码,我会更新我的答案。

编辑 2:为自己省去很多麻烦,并确保您<div contenteditable=true>display: inline-block. 这修复了一些与 Chrome 放置相关的错误,<div>而不是<br>在您按下 Enter 键时。

如何使用

let richText = document.getElementById('rich-text');
let offset = Cursor.getCurrentCursorPosition(richText);
// insert code here that does stuff to the innerHTML, such as adding/removing <span> tags
Cursor.setCurrentCursorPosition(offset, richText);
richText.focus();

代码

// Credit to Liam (Stack Overflow)
// https://stackoverflow.com/a/41034697/3480193
class Cursor {
    static getCurrentCursorPosition(parentElement) {
        var selection = window.getSelection(),
            charCount = -1,
            node;
        
        if (selection.focusNode) {
            if (Cursor._isChildOf(selection.focusNode, parentElement)) {
                node = selection.focusNode; 
                charCount = selection.focusOffset;
                
                while (node) {
                    if (node === parentElement) {
                        break;
                    }

                    if (node.previousSibling) {
                        node = node.previousSibling;
                        charCount += node.textContent.length;
                    } else {
                        node = node.parentNode;
                        if (node === null) {
                            break;
                        }
                    }
                }
            }
        }
        
        return charCount;
    }
    
    static setCurrentCursorPosition(chars, element) {
        if (chars >= 0) {
            var selection = window.getSelection();
            
            let range = Cursor._createRange(element, { count: chars });

            if (range) {
                range.collapse(false);
                selection.removeAllRanges();
                selection.addRange(range);
            }
        }
    }
    
    static _createRange(node, chars, range) {
        if (!range) {
            range = document.createRange()
            range.selectNode(node);
            range.setStart(node, 0);
        }

        if (chars.count === 0) {
            range.setEnd(node, chars.count);
        } else if (node && chars.count >0) {
            if (node.nodeType === Node.TEXT_NODE) {
                if (node.textContent.length < chars.count) {
                    chars.count -= node.textContent.length;
                } else {
                    range.setEnd(node, chars.count);
                    chars.count = 0;
                }
            } else {
                for (var lp = 0; lp < node.childNodes.length; lp++) {
                    range = Cursor._createRange(node.childNodes[lp], chars, range);

                    if (chars.count === 0) {
                    break;
                    }
                }
            }
        } 

        return range;
    }
    
    static _isChildOf(node, parentElement) {
        while (node !== null) {
            if (node === parentElement) {
                return true;
            }
            node = node.parentNode;
        }

        return false;
    }
}
Enter在上面的代码中是否有另一种方法来处理命中就我而言,这真的很不方便。
2021-03-31 13:35:18
@bgplaya 我实际上用这段代码提出了一个单独的问题,并提供了一个赏金来修复输入错误。没有人能够修复它。
2021-04-05 13:35:18

我正在编写语法高亮器(和基本代码编辑器),我需要知道如何自动键入单引号字符并将插入符号移回(就像现在的许多代码编辑器一样)。

这是我的解决方案的一个片段,多亏了这个线程、MDN 文档和很多 moz 控制台的帮助。

//onKeyPress event

if (evt.key === "\"") {
    let sel = window.getSelection();
    let offset = sel.focusOffset;
    let focus = sel.focusNode;

    focus.textContent += "\""; //setting div's innerText directly creates new
    //nodes, which invalidate our selections, so we modify the focusNode directly

    let range = document.createRange();
    range.selectNode(focus);
    range.setStart(focus, offset);

    range.collapse(true);
    sel.removeAllRanges();
    sel.addRange(range);
}

//end onKeyPress event

这是在一个内容可编辑的 div 元素中

我把它留在这里作为感谢,意识到已经有一个可以接受的答案。

  const el = document.getElementById("editable");
  el.focus()
  let char = 1, sel; // character at which to place caret

  if (document.selection) {
    sel = document.selection.createRange();
    sel.moveStart('character', char);
    sel.select();
  }
  else {
    sel = window.getSelection();
    sel.collapse(el.lastChild, char);
  }