Skip to content

Instantly share code, notes, and snippets.

@Yaffle
Last active September 8, 2024 12:56
Show Gist options
  • Save Yaffle/117a0f6f92976b5cc6a6f570d911d912 to your computer and use it in GitHub Desktop.
Save Yaffle/117a0f6f92976b5cc6a6f570d911d912 to your computer and use it in GitHub Desktop.
get/set caret position in contenteditable element
// Usage:
// var x = document.querySelector('[contenteditable]');
// var caretPosition = getSelectionDirection(x) !== 'forward' ? getSelectionStart(x) : getSelectionEnd(x);
// setSelectionRange(x, caretPosition + 1, caretPosition + 1);
// var value = getValue(x);
// it will not work with "<img /><img />" and, perhaps, in many other cases.
function isAfter(container, offset, node) {
var c = node;
while (c.parentNode != container) {
c = c.parentNode;
}
var i = offset;
while (c != null && i > 0) {
c = c.previousSibling;
i -= 1;
}
return i > 0;
}
function compareCaretPositons(node1, offset1, node2, offset2) {
if (node1 === node2) {
return offset1 - offset2;
}
var c = node1.compareDocumentPosition(node2);
if ((c & Node.DOCUMENT_POSITION_CONTAINED_BY) !== 0) {
return isAfter(node1, offset1, node2) ? +1 : -1;
} else if ((c & Node.DOCUMENT_POSITION_CONTAINS) !== 0) {
return isAfter(node2, offset2, node1) ? -1 : +1;
} else if ((c & Node.DOCUMENT_POSITION_FOLLOWING) !== 0) {
return -1;
} else if ((c & Node.DOCUMENT_POSITION_PRECEDING) !== 0) {
return +1;
}
}
function stringifyElementStart(node, isLineStart) {
if (node.tagName.toLowerCase() === 'br') {
if (true) {
return '\n';
}
}
if (node.tagName.toLowerCase() === 'div') { // Is a block-level element?
if (!isLineStart) { //TODO: Is not at start of a line?
return '\n';
}
}
return '';
}
function* positions(node, isLineStart = true) {
console.assert(node.nodeType === Node.ELEMENT_NODE);
var child = node.firstChild;
var offset = 0;
yield {node: node, offset: offset, text: stringifyElementStart(node, isLineStart)};
while (child != null) {
if (child.nodeType === Node.TEXT_NODE) {
yield {node: child, offset: 0/0, text: child.data};
isLineStart = false;
} else {
isLineStart = yield* positions(child, isLineStart);
}
child = child.nextSibling;
offset += 1;
yield {node: node, offset: offset, text: ''};
}
return isLineStart;
}
function getCaretPosition(contenteditable, textPosition) {
var textOffset = 0;
var lastNode = null;
var lastOffset = 0;
for (var p of positions(contenteditable)) {
if (p.text.length > textPosition - textOffset) {
return {node: p.node, offset: p.node.nodeType === Node.TEXT_NODE ? textPosition - textOffset : p.offset};
}
textOffset += p.text.length;
lastNode = p.node;
lastOffset = p.node.nodeType === Node.TEXT_NODE ? p.text.length : p.offset;
}
return {node: lastNode, offset: lastOffset};
}
function getTextOffset(contenteditable, selectionNode, selectionOffset) {
var textOffset = 0;
for (var p of positions(contenteditable)) {
if (selectionNode.nodeType !== Node.TEXT_NODE && selectionNode === p.node && selectionOffset === p.offset) {
return textOffset;
}
if (selectionNode.nodeType === Node.TEXT_NODE && selectionNode === p.node) {
return textOffset + selectionOffset;
}
textOffset += p.text.length;
}
return compareCaretPositons(selectionNode, selectionOffset, contenteditable, 0) < 0 ? 0 : textOffset;
}
function getValue(contenteditable) {
var value = '';
for (var p of positions(contenteditable)) {
value += p.text;
}
return value;
}
function setSelectionRange(contenteditable, start, end) {
var selection = window.getSelection();
var s = getCaretPosition(contenteditable, start);
var e = getCaretPosition(contenteditable, end);
selection.setBaseAndExtent(s.node, s.offset, e.node, e.offset);
}
//TODO: Ctrl+A - rangeCount is 2
function getSelectionDirection(contenteditable) {
var selection = window.getSelection();
var c = compareCaretPositons(selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset);
return c < 0 ? 'forward' : 'none';
}
function getSelectionStart(contenteditable) {
var selection = window.getSelection();
var c = compareCaretPositons(selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset);
return c < 0 ? getTextOffset(contenteditable, selection.anchorNode, selection.anchorOffset) : getTextOffset(contenteditable, selection.focusNode, selection.focusOffset);
}
function getSelectionEnd(contenteditable) {
var selection = window.getSelection();
var c = compareCaretPositons(selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset);
return c < 0 ? getTextOffset(contenteditable, selection.focusNode, selection.focusOffset) : getTextOffset(contenteditable, selection.anchorNode, selection.anchorOffset);
}
@abohomol
Copy link

@Yaffle, thanks for the gist. Where does the getSelectionRange come from?

@Yaffle
Copy link
Author

Yaffle commented Jul 31, 2020

@abohomol, ups,... I have updated the gist, it was called getTextSelection

@abohomol
Copy link

abohomol commented Aug 4, 2020

@Yaffle Thanks, it may also not work if each line is wrapped in a div, which results in a line break, I'll need to update the code a bit to make it work for me.

@abohomol
Copy link

abohomol commented Aug 4, 2020

@Yaffle may I ask why do you add +1 to start/end when you set the position?

@Yaffle
Copy link
Author

Yaffle commented Aug 4, 2020

@abohomol, in the example? just to change the position

@Yaffle
Copy link
Author

Yaffle commented Aug 4, 2020

@abohomol, I have updated a code to support some simple cases with <div> elements

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment