在 contenteditable 插入符位置考虑 `<br>`
Accounting for `<br>`s in contenteditable caret position
为了获取和设置 contenteditable 元素中的插入符位置,我已经尝试了 this answer 中的代码,但是开始和结束位置会随着您移动到不同的文本节点而重置。
<div contenteditable>012345<br><br><br>9012345</div>
所以,我修改了 this answer (by @TimDown) but it's still not quite counting the line breaks properly... In this demo 的代码,当我在 4
之后单击并按三下向右箭头时,我将看到 start/end 报告为 5
、6
,然后是 8
。或者,使用鼠标从第一行的 4
开始 select 并继续向右 selecting(参见 gif)
这是代码(demo;尽管看起来像,jQuery 没有被使用)
function getCaret(el) {
let start, end;
const range = document.getSelection().getRangeAt(0),
preSelectionRange = range.cloneRange(),
postSelectionRange = range.cloneRange();
preSelectionRange.selectNodeContents(el);
preSelectionRange.setEnd(range.startContainer, range.startOffset);
postSelectionRange.selectNodeContents(el);
postSelectionRange.setEnd(range.endContainer, range.endOffset);
start = preSelectionRange.toString().length;
end = start + range.toString().length;
// count <br>'s and adjust start & end
if (start > 0) {
var node,
i = el.children.length;
while (i--) {
node = el.children[i];
if (node.nodeType === 1 && node.nodeName === 'BR') {
start += preSelectionRange.intersectsNode(el.children[i]) ? 1 : 0;
end += postSelectionRange.intersectsNode(el.children[i]) ? 1 : 0;
}
}
}
return {start, end};
}
setCaret
函数修改似乎工作正常(在这个基本的内容可编辑示例中)。
function setCaret(el, start, end) {
var node, i, nextCharIndex, sel,
charIndex = 0,
nodeStack = [el],
foundStart = false,
stop = false,
range = document.createRange();
range.setStart(el, 0);
range.collapse(true);
while (!stop && (node = nodeStack.pop())) {
// BR's aren't counted, so we need to increase the index when one
// is encountered
if (node.nodeType === 1 && node.nodeName === 'BR') {
charIndex++;
} else if (node.nodeType === 3) {
nextCharIndex = charIndex + node.length;
if (!foundStart && start >= charIndex && start <= nextCharIndex) {
range.setStart(node, start - charIndex);
foundStart = true;
}
if (foundStart && end >= charIndex && end <= nextCharIndex) {
range.setEnd(node, end - charIndex);
stop = true;
}
charIndex = nextCharIndex;
} else {
i = node.childNodes.length;
while (i--) {
nodeStack.push(node.childNodes[i]);
}
}
}
sel = document.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
我可以使用一些 advice/help 解决以下问题:
- 如何正确计算
<br>
秒?
如何计算开头的<br>
(在此HTML示例中)?
<div contenteditable><br>12345<br><br><br>9012345</div>
将 <br>
包裹在 <div>
中(在此 HTML 示例中)- 我最终会讲到这一点,但我没有想继续沿着这条路走下去,发现有更简单的方法。
<div contenteditable><div><br></div>12345<div><br></div><div><br></div><div><br></div>9012345</div>
我试图用rangy
替换上面的代码,但它似乎没有内置方法来获取或设置范围。
我修改了您的演示,将位置序列化为 container/offset 对,而不仅仅是一个位置。容器被序列化为一个简单的索引数组,进入每个节点的 childNodes
集合,从一个参考节点开始(在本例中当然是 contenteditable
元素)。
我不太清楚你打算用它做什么,但由于它反映了选择模型,它应该会给你带来更少的痛苦。
const $el = $('ce'),
$startContainer = $('start-container'),
$startOffset = $('start-offset'),
$endContainer = $('end-container'),
$endOffset = $('end-offset');
function pathFromNode(node, reference) {
function traverse(node, acc) {
if (node === reference) {
return acc;
} else {
const parent = node.parentNode;
const index = [...parent.childNodes].indexOf(node);
return traverse(parent, [index, ...acc]);
}
}
return traverse(node, []);
}
function nodeFromPath(path, reference) {
if (path.length === 0) {
return reference;
} else {
const [index, ...rest] = path;
const next = reference.childNodes[index];
return nodeFromPath(rest, next);
}
}
function getCaret(el) {
const range = document.getSelection().getRangeAt(0);
return {
start: {
container: pathFromNode(range.startContainer, el),
offset: range.startOffset
},
end: {
container: pathFromNode(range.endContainer, el),
offset: range.endOffset
}
};
}
function setCaret(el, start, end) {
const range = document.createRange();
range.setStart(nodeFromPath(start.container, el), start.offset);
range.setEnd(nodeFromPath(end.container, el), end.offset);
sel = document.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
function update() {
const pos = getCaret($el);
$startContainer.value = JSON.stringify(pos.start.container);
$startOffset.value = pos.start.offset;
$endContainer.value = JSON.stringify(pos.end.container);
$endOffset.value = pos.end.offset;
}
$el.addEventListener('keyup', update);
$el.addEventListener('click', update);
$('set').addEventListener('click', () => {
const start = {
container: JSON.parse($startContainer.value),
offset: $startOffset.value
};
const end = {
container: JSON.parse($endContainer.value),
offset: $endOffset.value
};
setCaret($el, start, end);
});
function $(sel) {
return document.getElementById(sel);
}
input {
width: 40px;
}
[contenteditable] {
white-space: pre;
}
(updates on click & keyup)<br/>
<label>Start: <input id="start-container" type="text"/><input id="start-offset" type="number"/></label><br/>
<label>End: <input id="end-container" type="text"/><input id="end-offset" type="number"/></label><br/>
<button id="set">Set</button>
<p></p>
<!-- inline BR's behave differently from <br> on their own separate line
<div id="ce" contenteditable>012345<br><br><br>9012345</div>
-->
<!-- get/set caret needs to work with these examples as well
* <br> at beginning
<div id="ce" contenteditable><br>12345<br><br><br>9012345</div>
* <br>'s wrapped in a <div>
-->
<div id="ce" contenteditable><div><br></div>12345<div><br></div><div><br></div><div><br></div>9012345</div>
为了获取和设置 contenteditable 元素中的插入符位置,我已经尝试了 this answer 中的代码,但是开始和结束位置会随着您移动到不同的文本节点而重置。
<div contenteditable>012345<br><br><br>9012345</div>
所以,我修改了 this answer (by @TimDown) but it's still not quite counting the line breaks properly... In this demo 的代码,当我在 4
之后单击并按三下向右箭头时,我将看到 start/end 报告为 5
、6
,然后是 8
。或者,使用鼠标从第一行的 4
开始 select 并继续向右 selecting(参见 gif)
这是代码(demo;尽管看起来像,jQuery 没有被使用)
function getCaret(el) {
let start, end;
const range = document.getSelection().getRangeAt(0),
preSelectionRange = range.cloneRange(),
postSelectionRange = range.cloneRange();
preSelectionRange.selectNodeContents(el);
preSelectionRange.setEnd(range.startContainer, range.startOffset);
postSelectionRange.selectNodeContents(el);
postSelectionRange.setEnd(range.endContainer, range.endOffset);
start = preSelectionRange.toString().length;
end = start + range.toString().length;
// count <br>'s and adjust start & end
if (start > 0) {
var node,
i = el.children.length;
while (i--) {
node = el.children[i];
if (node.nodeType === 1 && node.nodeName === 'BR') {
start += preSelectionRange.intersectsNode(el.children[i]) ? 1 : 0;
end += postSelectionRange.intersectsNode(el.children[i]) ? 1 : 0;
}
}
}
return {start, end};
}
setCaret
函数修改似乎工作正常(在这个基本的内容可编辑示例中)。
function setCaret(el, start, end) {
var node, i, nextCharIndex, sel,
charIndex = 0,
nodeStack = [el],
foundStart = false,
stop = false,
range = document.createRange();
range.setStart(el, 0);
range.collapse(true);
while (!stop && (node = nodeStack.pop())) {
// BR's aren't counted, so we need to increase the index when one
// is encountered
if (node.nodeType === 1 && node.nodeName === 'BR') {
charIndex++;
} else if (node.nodeType === 3) {
nextCharIndex = charIndex + node.length;
if (!foundStart && start >= charIndex && start <= nextCharIndex) {
range.setStart(node, start - charIndex);
foundStart = true;
}
if (foundStart && end >= charIndex && end <= nextCharIndex) {
range.setEnd(node, end - charIndex);
stop = true;
}
charIndex = nextCharIndex;
} else {
i = node.childNodes.length;
while (i--) {
nodeStack.push(node.childNodes[i]);
}
}
}
sel = document.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
我可以使用一些 advice/help 解决以下问题:
- 如何正确计算
<br>
秒? 如何计算开头的
<br>
(在此HTML示例中)?<div contenteditable><br>12345<br><br><br>9012345</div>
将
<br>
包裹在<div>
中(在此 HTML 示例中)- 我最终会讲到这一点,但我没有想继续沿着这条路走下去,发现有更简单的方法。<div contenteditable><div><br></div>12345<div><br></div><div><br></div><div><br></div>9012345</div>
我试图用
rangy
替换上面的代码,但它似乎没有内置方法来获取或设置范围。
我修改了您的演示,将位置序列化为 container/offset 对,而不仅仅是一个位置。容器被序列化为一个简单的索引数组,进入每个节点的 childNodes
集合,从一个参考节点开始(在本例中当然是 contenteditable
元素)。
我不太清楚你打算用它做什么,但由于它反映了选择模型,它应该会给你带来更少的痛苦。
const $el = $('ce'),
$startContainer = $('start-container'),
$startOffset = $('start-offset'),
$endContainer = $('end-container'),
$endOffset = $('end-offset');
function pathFromNode(node, reference) {
function traverse(node, acc) {
if (node === reference) {
return acc;
} else {
const parent = node.parentNode;
const index = [...parent.childNodes].indexOf(node);
return traverse(parent, [index, ...acc]);
}
}
return traverse(node, []);
}
function nodeFromPath(path, reference) {
if (path.length === 0) {
return reference;
} else {
const [index, ...rest] = path;
const next = reference.childNodes[index];
return nodeFromPath(rest, next);
}
}
function getCaret(el) {
const range = document.getSelection().getRangeAt(0);
return {
start: {
container: pathFromNode(range.startContainer, el),
offset: range.startOffset
},
end: {
container: pathFromNode(range.endContainer, el),
offset: range.endOffset
}
};
}
function setCaret(el, start, end) {
const range = document.createRange();
range.setStart(nodeFromPath(start.container, el), start.offset);
range.setEnd(nodeFromPath(end.container, el), end.offset);
sel = document.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
function update() {
const pos = getCaret($el);
$startContainer.value = JSON.stringify(pos.start.container);
$startOffset.value = pos.start.offset;
$endContainer.value = JSON.stringify(pos.end.container);
$endOffset.value = pos.end.offset;
}
$el.addEventListener('keyup', update);
$el.addEventListener('click', update);
$('set').addEventListener('click', () => {
const start = {
container: JSON.parse($startContainer.value),
offset: $startOffset.value
};
const end = {
container: JSON.parse($endContainer.value),
offset: $endOffset.value
};
setCaret($el, start, end);
});
function $(sel) {
return document.getElementById(sel);
}
input {
width: 40px;
}
[contenteditable] {
white-space: pre;
}
(updates on click & keyup)<br/>
<label>Start: <input id="start-container" type="text"/><input id="start-offset" type="number"/></label><br/>
<label>End: <input id="end-container" type="text"/><input id="end-offset" type="number"/></label><br/>
<button id="set">Set</button>
<p></p>
<!-- inline BR's behave differently from <br> on their own separate line
<div id="ce" contenteditable>012345<br><br><br>9012345</div>
-->
<!-- get/set caret needs to work with these examples as well
* <br> at beginning
<div id="ce" contenteditable><br>12345<br><br><br>9012345</div>
* <br>'s wrapped in a <div>
-->
<div id="ce" contenteditable><div><br></div>12345<div><br></div><div><br></div><div><br></div>9012345</div>