Last active
September 8, 2024 12:56
-
-
Save Yaffle/117a0f6f92976b5cc6a6f570d911d912 to your computer and use it in GitHub Desktop.
get/set caret position in contenteditable element
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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, ups,... I have updated the gist, it was called getTextSelection
@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.
@Yaffle may I ask why do you add +1 to start/end when you set the position?
@abohomol, in the example? just to change the position
@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
@Yaffle, thanks for the gist. Where does the
getSelectionRange
come from?