如何设置插入符号(光标)的位置在一个可满足的元素(div)?

我有一个简单的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):

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

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

223477 次浏览

在大多数浏览器中,你需要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 & lt;9的工作方式完全不同。如果需要支持这些浏览器,则需要不同的代码。

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

function set_mouse() {
var as = document.getElementById("editable");
el = as.childNodes[1].childNodes[0]; //goal is to get ('we') id to write (object Text) because it work only in object text
var range = document.createRange();
var sel = window.getSelection();
range.setStart(el, 1);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);


document.getElementById("we").innerHTML = el; // see out put of we id
}
<div id="editable" contenteditable="true">dddddddddddddddddddddddddddd
<p>dd</p>psss
<p>dd</p>
<p>dd</p>
<p>text text text</p>
</div>
<p id='we'></p>
<button onclick="set_mouse()">focus</button>

当你有(p) (span)等高级元素时,将插入符号设置在合适的位置是非常困难的。目标是获取(对象文本):

<div id="editable" contenteditable="true">dddddddddddddddddddddddddddd<p>dd</p>psss<p>dd</p>
<p>dd</p>
<p>text text text</p>
</div>
<p id='we'></p>
<button onclick="set_mouse()">focus</button>
<script>


function set_mouse() {
var as = document.getElementById("editable");
el = as.childNodes[1].childNodes[0];//goal is to get ('we') id to write (object Text) because it work only in object text
var range = document.createRange();
var sel = window.getSelection();
range.setStart(el, 1);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);


document.getElementById("we").innerHTML = el;// see out put of we id
}
</script>

您在可满足光标定位方面找到的大多数答案都相当简单,因为它们只满足带有普通文本的输入。一旦在容器中使用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对它进行了测试,它们都运行良好。

PS.如果有人感兴趣,我得到当前的光标位置使用这段代码:

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,并向后计算遇到的所有文本字符,直到它到达id为containerId的父节点。isChildOf函数只是在运行之前检查所提供的节点实际上是所提供的parentId的子节点。

代码应该工作直接没有改变,但我刚刚从一个jQuery插件我已经开发了,所以已经黑出了一对这是 -让我知道,如果有什么不工作!

如果你不想使用jQuery,你可以试试这个方法:

public setCaretPosition() {
const editableDiv = document.getElementById('contenteditablediv');
const lastLine = this.input.nativeElement.innerHTML.replace(/.*?(<br>)/g, '');
const selection = window.getSelection();
selection.collapse(editableDiv.childNodes[editableDiv.childNodes.length - 1], lastLine.length);
}

editableDiv你的可编辑元素,不要忘记为它设置一个id。然后你需要从元素中得到你的innerHTML,并切断所有的刹车线。然后用next参数设置collapse。

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

这里是我的解决方案的一个片段,感谢来自这个线程的帮助,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);
}

我认为将插入符号设置到可满足元素中的某个位置并不简单。我为此写了自己的代码。它绕过了计算剩余字符数的节点树,并在所需元素中设置插入符号。我没有对这段代码进行太多测试。

//Set offset in current contenteditable field (for start by default or for with forEnd=true)
function setCurSelectionOffset(offset, forEnd = false) {
const sel = window.getSelection();
if (sel.rangeCount !== 1 || !document.activeElement) return;


const firstRange = sel.getRangeAt(0);


if (offset > 0) {
bypassChildNodes(document.activeElement, offset);
}else{
if (forEnd)
firstRange.setEnd(document.activeElement, 0);
else
firstRange.setStart(document.activeElement, 0);
}






//Bypass in depth
function bypassChildNodes(el, leftOffset) {
const childNodes = el.childNodes;


for (let i = 0; i < childNodes.length && leftOffset; i++) {
const childNode = childNodes[i];


if (childNode.nodeType === 3) {
const curLen = childNode.textContent.length;


if (curLen >= leftOffset) {
if (forEnd)
firstRange.setEnd(childNode, leftOffset);
else
firstRange.setStart(childNode, leftOffset);
return 0;
}else{
leftOffset -= curLen;
}
}else
if (childNode.nodeType === 1) {
leftOffset = bypassChildNodes(childNode, leftOffset);
}
}


return leftOffset;
}
}

我还写了代码来获取当前插入符号的位置(没有测试):

//Get offset in current contenteditable field (start offset by default or end offset with calcEnd=true)
function getCurSelectionOffset(calcEnd = false) {
const sel = window.getSelection();
if (sel.rangeCount !== 1 || !document.activeElement) return 0;


const firstRange     = sel.getRangeAt(0),
startContainer = calcEnd ? firstRange.endContainer : firstRange.startContainer,
startOffset    = calcEnd ? firstRange.endOffset    : firstRange.startOffset;
let needStop = false;


return bypassChildNodes(document.activeElement);






//Bypass in depth
function bypassChildNodes(el) {
const childNodes = el.childNodes;
let ans = 0;


if (el === startContainer) {
if (startContainer.nodeType === 3) {
ans = startOffset;
}else
if (startContainer.nodeType === 1) {
for (let i = 0; i < startOffset; i++) {
const childNode = childNodes[i];


ans += childNode.nodeType === 3 ? childNode.textContent.length :
childNode.nodeType === 1 ? childNode.innerText.length :
0;
}
}


needStop = true;
}else{
for (let i = 0; i < childNodes.length && !needStop; i++) {
const childNode = childNodes[i];
ans += bypassChildNodes(childNode);
}
}


return ans;
}
}

你还需要注意范围。starttooffset和range。endOffset包含文本节点(nodeType === 3)的字符偏移量和元素节点(nodeType === 1)的子节点偏移量。startContainer和range。endContainer可以引用树中任何级别的任何元素节点(当然它们也可以引用文本节点)。

基于Tim Down的答案,但它会检查最后一个已知的“好”文本行。它把光标放在最后。

此外,我还可以递归/迭代地检查每个连续的最后一个子节点的最后一个子节点,以找到DOM中绝对最后一个“好”文本节点。

function onClickHandler() {
setCaret(document.getElementById("editable"));
}


function setCaret(el) {
let range = document.createRange(),
sel = window.getSelection(),
lastKnownIndex = -1;
for (let i = 0; i < el.childNodes.length; i++) {
if (isTextNodeAndContentNoEmpty(el.childNodes[i])) {
lastKnownIndex = i;
}
}
if (lastKnownIndex === -1) {
throw new Error('Could not find valid text content');
}
let row = el.childNodes[lastKnownIndex],
col = row.textContent.length;
range.setStart(row, col);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
el.focus();
}


function isTextNodeAndContentNoEmpty(node) {
return node.nodeType == Node.TEXT_NODE && node.textContent.trim().length > 0
}
<div id="editable" contenteditable="true">
text text text<br>text text text<br>text text text<br>
</div>
<button id="button" onclick="onClickHandler()">focus</button>

我为我的简单文本编辑器做了这个。

与其他方法的区别:

  • 高性能
  • 适用于所有空间

使用

// get current selection
const [start, end] = getSelectionOffset(container)


// change container html
container.innerHTML = newHtml


// restore selection
setSelectionOffset(container, start, end)


// use this instead innerText for get text with keep all spaces
const innerText = getInnerText(container)
const textBeforeCaret = innerText.substring(0, start)
const textAfterCaret = innerText.substring(start)

selection.ts

/** return true if node found */
function searchNode(
container: Node,
startNode: Node,
predicate: (node: Node) => boolean,
excludeSibling?: boolean,
): boolean {
if (predicate(startNode as Text)) {
return true
}


for (let i = 0, len = startNode.childNodes.length; i < len; i++) {
if (searchNode(startNode, startNode.childNodes[i], predicate, true)) {
return true
}
}


if (!excludeSibling) {
let parentNode = startNode
while (parentNode && parentNode !== container) {
let nextSibling = parentNode.nextSibling
while (nextSibling) {
if (searchNode(container, nextSibling, predicate, true)) {
return true
}
nextSibling = nextSibling.nextSibling
}
parentNode = parentNode.parentNode
}
}


return false
}


function createRange(container: Node, start: number, end: number): Range {
let startNode
searchNode(container, container, node => {
if (node.nodeType === Node.TEXT_NODE) {
const dataLength = (node as Text).data.length
if (start <= dataLength) {
startNode = node
return true
}
start -= dataLength
end -= dataLength
return false
}
})


let endNode
if (startNode) {
searchNode(container, startNode, node => {
if (node.nodeType === Node.TEXT_NODE) {
const dataLength = (node as Text).data.length
if (end <= dataLength) {
endNode = node
return true
}
end -= dataLength
return false
}
})
}


const range = document.createRange()
if (startNode) {
if (start < startNode.data.length) {
range.setStart(startNode, start)
} else {
range.setStartAfter(startNode)
}
} else {
if (start === 0) {
range.setStart(container, 0)
} else {
range.setStartAfter(container)
}
}


if (endNode) {
if (end < endNode.data.length) {
range.setEnd(endNode, end)
} else {
range.setEndAfter(endNode)
}
} else {
if (end === 0) {
range.setEnd(container, 0)
} else {
range.setEndAfter(container)
}
}


return range
}


export function setSelectionOffset(node: Node, start: number, end: number) {
const range = createRange(node, start, end)
const selection = window.getSelection()
selection.removeAllRanges()
selection.addRange(range)
}


function hasChild(container: Node, node: Node): boolean {
while (node) {
if (node === container) {
return true
}
node = node.parentNode
}


return false
}


function getAbsoluteOffset(container: Node, offset: number) {
if (container.nodeType === Node.TEXT_NODE) {
return offset
}


let absoluteOffset = 0
for (let i = 0, len = Math.min(container.childNodes.length, offset); i < len; i++) {
const childNode = container.childNodes[i]
searchNode(childNode, childNode, node => {
if (node.nodeType === Node.TEXT_NODE) {
absoluteOffset += (node as Text).data.length
}
return false
})
}


return absoluteOffset
}


export function getSelectionOffset(container: Node): [number, number] {
let start = 0
let end = 0


const selection = window.getSelection()
for (let i = 0, len = selection.rangeCount; i < len; i++) {
const range = selection.getRangeAt(i)
if (range.intersectsNode(container)) {
const startNode = range.startContainer
searchNode(container, container, node => {
if (startNode === node) {
start += getAbsoluteOffset(node, range.startOffset)
return true
}


const dataLength = node.nodeType === Node.TEXT_NODE
? (node as Text).data.length
: 0


start += dataLength
end += dataLength


return false
})


const endNode = range.endContainer
searchNode(container, startNode, node => {
if (endNode === node) {
end += getAbsoluteOffset(node, range.endOffset)
return true
}


const dataLength = node.nodeType === Node.TEXT_NODE
? (node as Text).data.length
: 0


end += dataLength


return false
})


break
}
}


return [start, end]
}


export function getInnerText(container: Node) {
const buffer = []
searchNode(container, container, node => {
if (node.nodeType === Node.TEXT_NODE) {
buffer.push((node as Text).data)
}
return false
})
return buffer.join('')
}

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

这段代码特别适用于在富文本框中固定光标,该文本框可能是用<div contenteditable="true">创建的。在得到下面的代码之前,我被这个问题困了好几天。

编辑:他的答案和这个答案有一个bug,涉及到按回车键。由于enter不能算作一个字符,所以按下enter键后光标的位置就乱了。如果我能够修复代码,我将更新我的答案。

edit2:为自己省去很多麻烦,确保你的<div contenteditable=true>display: inline-block。这修复了一些与Chrome在你按enter键时将<div>而不是<br>相关的错误。

如何使用

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;
}
}
var sel = window.getSelection();
sel?.setPosition(wordDiv.childNodes[0], 5);
event.preventDefault();
move(element:any,x:number){//parent
let arr:Array<any>=[];
arr=this.getAllnodeOfanItem(this.input.nativeElement,arr);
let j=0;
while (x>arr[j].length && j<arr.length){
x-=arr[j].length;
j++;
}
    



    

var el = arr[j];
var range = document.createRange();
var sel = window.getSelection();
range.setStart(el,x );
range.collapse(true);
if (sel)sel.removeAllRanges();
if (sel)sel.addRange(range);
}


getAllnodeOfanItem(element:any,rep:Array<any>){
let ch:Array<any>=element.childNodes;
if (ch.length==0 && element.innerText!="")
rep.push(element);
else{
for (let i=0;i<ch.length;i++){
rep=this.getAllnodeOfanItem(ch[i],rep)
}
}
return rep;
}

我已经阅读并尝试了这里的一些案例,只是把对我有用的放在这里,根据dom节点考虑一些细节:

  focus(textInput){
const length = textInput.innerText.length;
textInput.focus();


if(!!textInput.lastChild){
const sel = window.getSelection();
sel.collapse(textInput.lastChild, length);
}
}