To get and set the caret position in an contenteditable element, I've tried the code from this answer, but the start & end position resets as you move into different text nodes.
<div contenteditable>012345<br><br><br>9012345</div>
So, I modified the code from this answer (by @TimDown) but it's still not quite counting the line breaks properly... In this demo, when I click after the 4
and press the right arrow three times, I'll see the start/end report as 5
, 6
, then 8
. Or, use the mouse to select from the 4
in the first row and continuing selecting to the right (see gif)
Here is the code (demo; even though it looks like it, jQuery is not being used)
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};
}
The setCaret
function modification appears to be working properly (in this basic contenteditable example).
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);
}
I could use some advice/help with the following issues:
<br>
s?How do you count a <br>
at the beginning (in this HTML example)?
<div contenteditable><br>12345<br><br><br>9012345</div>
Include <br>
's wrapped in a <div>
(in this HTML example) - I'll eventually get to this, but I didn't want to continue down this path and find out there is an easier method.
<div contenteditable><div><br></div>12345<div><br></div><div><br></div><div><br></div>9012345</div>
I tried to replace the above code with rangy
, but it doesn't appear to have a built-in method to get or set a range.
I modified your demo to serialize the position as a container/offset pair instead of just a position. The container is serialized as a simple array of indexes into the childNodes
collection of each node starting from a reference node (which in this case is the contenteditable
element, of course).
It's not completely clear to me what you intend to use this for, but since it mirrors the selection model it should hopefully give you much less pain.
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>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With