可编辑插入符号位置

我找到了大量的好的,跨浏览器的答案,如何 准备好了插入符号的位置在 contentEditable元素,但没有一个如何 走开插入符号的位置摆在首位。

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

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


$('#contentbox').keyup(function() {
// ... ?
});
167035 次浏览
//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 的内容发生变化。

IE8及以下版本的参考资料: Http://msdn.microsoft.com/en-us/library/ms535872(vs.85).aspx

标准(所有其他)浏览器参考: Https://developer.mozilla.org/en/dom/range (这是 mozilla 文档,但代码也可以在 chrome、 safari、 Opera 和 ie9中使用)

以下守则假定:

  • 在可编辑的 <div>中总是有一个文本节点,而没有其他节点
  • 可编辑的 div 没有将 CSS white-space属性设置为 pre

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

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>

试试这个:

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

Https://github.com/ichord/caret.js

演示: Http://ichord.github.com/caret.js

GetSelection-vs-document. select

这个适合我:

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());

鼠标事件使用:

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

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

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
};
}

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

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

  1. 元素可以包含多个子节点级别(例如,子节点包含子节点包含子节点...)
  2. 一个选择可以由不同的开始和结束位置组成(例如,选择了多个字符)
  3. 包含 Caret start/end 的节点可能既不是元素,也不是它的直接子元素

这里有一种方法可以让开始和结束位置作为元素 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]];
}

来得有点晚了,不过以防其他人遇到麻烦。在过去的两天里,我找到的所有谷歌搜索都没有找到任何有用的东西,但是我想出了一个简洁而优雅的解决方案,不管你有多少嵌套标签,它都会一直有用:

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! :)

因为我花了很长时间才弄明白如何使用新的 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。然而,请注意,快速方向键的按下,以及快速删除似乎是跳过事件。

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

注意: 即需要一个 聚合填充物来支持 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;
}
}

如果您将可编辑 div 样式设置为“ display: inline-block; white-space: pre-wra”,那么在输入新行时不会得到新的子 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”时,它会创建一个新节点,因此 focus usOffset 会重置为零。这就是为什么我必须添加一个 range 变量,并将其从子节点的 focus Offset 扩展回 eDiv 的开始(从而捕获中间的所有文本)。

这个解决方案建立在@alockwood05的答案基础之上,为插入符号提供 get 和 set 功能,插入符号在 contenteditable div 中嵌套标记,并在节点中嵌套偏移量,这样你就有了一个既可序列化又可通过偏移量反序列化的解决方案。

我在一个跨平台的代码编辑器中使用这个解决方案,它需要在通过 lexer/parser 语法突显之前获取插入符号的开始/结束位置,然后立即将其设置回去。

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;
}

试试这种方法,以获得 来自 ContentEditableDiv 的插入符号位置

描述:

  1. 我为 Angular 编写了这段代码,但它也适用于原生 HTML。
  2. 该代码仅为可编辑 div 中的 斯潘元素返回插入符号位置。

我的准则:

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;
}

代码如何工作:

示例场景: “ Hi there,this | is sample text”。

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

  1. 最初,从 GetSelection ()方法获取插入符号所在的选择区域。
  2. LocusOffSet 只返回 currentNode 文本长度,在 艾格。中 currentNode 是“ this”,返回4到 计数
  3. 我的方法是从当前节点回溯。因此,我循环前面的节点,即[“ there”,“ Hi”] ,并将其文本长度添加到 caretRevCount。
  4. 最后,在循环完成之后,caretRevCount 返回一个 sum 值,即 插入位置

此答案使用递归函数处理嵌套的文本元素

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

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

我使用了 约翰・欧内斯特的优秀代码,并根据自己的需要进行了一些修改:

  • 使用 TypeScript (在角度应用程序中) ;
  • 使用稍微不同的数据结构。

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

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

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

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

[编辑]似乎 range 的节点总是 Text 类型的,所以我稍微简化了一下代码,它允许获取节点长度,而不需要强制转换。

密码如下:

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;
}

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

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

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

对于列号,有三种获得“列号”的方法

  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")));
});

我希望这对某些人有所帮助,我几个小时都在揪头发,试图弄清楚如何做到这一点!