我以这个简单的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);
是否可以像这样设置手动插入位置?
答案 0 :(得分:218)
在大多数浏览器中,您需要Range
和Selection
个对象。您将每个选择边界指定为节点和该节点内的偏移量。例如,要将插入符号设置为第二行文本的第五个字符,您需要执行以下操作:
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);
IE&lt; 9完全不同。如果您需要支持这些浏览器,则需要使用不同的代码。
jsFiddle示例:http://jsfiddle.net/timdown/vXnCM/
答案 1 :(得分:45)
您在可信任的光标定位上找到的大多数答案都相当简单,因为它们只能满足普通文本的输入。在容器中使用html元素后,输入的文本将被拆分为节点,并在树结构中自由分布。
要设置光标位置,我有这个函数循环所提供节点内的所有子文本节点,并设置从初始节点的开始到 chars.count 字符的范围: / p>
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插件中获取它,因此已经破解了几个这个 - 如果有什么不起作用,请告诉我!
答案 2 :(得分:2)
当你有像(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>
答案 3 :(得分:2)
如果您不想使用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
并切断所有制动线。然后用下一个参数设置崩溃。
答案 4 :(得分:2)
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);
}
答案 5 :(得分:1)
我正在编写一个语法突出显示工具(和基本的代码编辑器),我需要知道如何自动键入单引号字符并将指针插入后退(就像当今的许多代码编辑器一样。)
在此提供了我的解决方案的摘录,这要归功于该线程,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元素中
我在这里感谢,意识到已经有了一个可以接受的答案。
答案 6 :(得分:1)
我是为我的简单文本编辑器制作的。
与其他方法的区别:
用法
// 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('')
}
答案 7 :(得分:1)
我重构了@Liam的答案。我把它放在带有静态方法的类中,使它的函数接收一个元素而不是一个#id以及其他一些小的调整。
此代码特别适合将光标固定在您可能使用<div contenteditable="true">
制作的富文本框中。在到达下面的代码之前,我被困了几天。
edit:他的答案和这个答案都有一个涉及按回车键的错误。由于Enter键不算作字符,因此点击Enter键后光标位置就会混乱。如果我能够解决该代码,我将更新我的答案。
edit2:省去很多麻烦,并确保您的<div contenteditable=true>
是display: inline-block
。这修复了一些与Chrome有关的错误,当您按Enter键时,它们会将<div>
代替<br>
。
let richText = document.getElementById('rich-text');
let offset = Cursor.getCurrentCursorPosition(richText);
// do 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;
}
}
答案 8 :(得分:0)
我认为将插入符号设置为contenteditable元素中的某个位置并不容易。我为此编写了自己的代码。它绕过节点树计算剩余的字符数,并在所需元素中设置脱字符号。我没有对该代码进行太多测试。
//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;
}
}
您还需要注意range.startOffset和range.endOffset包含文本节点的字符偏移量(nodeType === 3)和元素节点的子节点偏移量(nodeType === 1)。 range.startContainer和range.endContainer可以引用树中任何级别的任何元素节点(当然,它们也可以引用文本节点)。
答案 9 :(得分:0)
基于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>
答案 10 :(得分:0)
var sel = window.getSelection();
sel?.setPosition(wordDiv.childNodes[0], 5);
event.preventDefault();