我有一个简单的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);
是否可以像这样手动设置插入符号的位置?
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>
我认为将插入符号设置到可满足元素中的某个位置并不简单。我为此写了自己的代码。它绕过了计算剩余字符数的节点树,并在所需元素中设置插入符号。我没有对这段代码进行太多测试。
//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>
我重构了@Liam的答案。我把它放在一个带有静态方法的类中,我让它的函数接收一个元素而不是#id,还有其他一些小调整。
这段代码特别适用于在富文本框中固定光标,例如<div contenteditable="true">。在得到下面的代码之前,我被这个问题困了好几天。
编辑:他的答案和这个答案有一个bug,涉及到按回车键。由于enter不能算作一个字符,所以按下enter键后光标的位置就乱了。如果我能够修复代码,我将更新我的答案。
edit2:为自己省去很多麻烦,确保<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();
Code
// 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;
}
}