Document: caretPositionFromPoint() method
Limited availability
This feature is not Baseline because it does not work in some of the most widely-used browsers.
The caretPositionFromPoint()
method of the Document
interface returns a CaretPosition
object, containing the DOM node, along with the caret and caret's character offset within that node.
Syntax
caretPositionFromPoint(x, y)
caretPositionFromPoint(x, y, options)
Parameters
x
-
The horizontal coordinate of a point.
y
-
The vertical coordinate of a point.
options
Optional-
The following optional properties may also be specified.
shadowRoots
Optional-
An array of
ShadowRoot
objects. The method can return a caret position for a node that is defined within the shadow DOM of a supplied shadow root. If the caret position falls within a shadow root that is not supplied, the returnedCaretPosition
will be remapped to the node that is the host of the shadow root.
Return value
A CaretPosition
object or null
.
The returned value is null
if there is no viewport associated with the document, if the x
or y
are negative or outside of the viewport region, or if the coordinates indicate a point where no text insertion point indicator could be inserted.
Examples
Split text nodes at caret position in DOM
This example demonstrates how to get the caret position from a selected DOM node, use the position to split the node, and insert a line break between the two nodes.
The example uses caretPositionFromPoint()
to get the caret position if supported, with the non-standard Document.caretRangeFromPoint()
method as a fallback.
Note that some parts of the code are hidden, including code used for logging, as this is not useful for understanding this method.
HTML
The HTML defines a paragraph of text.
<p>
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy
eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam
voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita
kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
</p>
JavaScript
The method below first checks for document.caretPositionFromPoint
support and uses it to get the text node and offset at the caret position.
If the browser doesn't support that method, the code then checks for document.caretRangeFromPoint
, and uses that instead.
If the node at the caret position is a text node, the code then splits the node into two at the selected offset, and inserts a line break between the two nodes.
function insertBreakAtPoint(e) {
let range;
let textNode;
let offset;
if (document.caretPositionFromPoint) {
range = document.caretPositionFromPoint(e.clientX, e.clientY);
textNode = range.offsetNode;
offset = range.offset;
} else if (document.caretRangeFromPoint) {
// Use WebKit-proprietary fallback method
range = document.caretRangeFromPoint(e.clientX, e.clientY);
textNode = range.startContainer;
offset = range.startOffset;
} else {
// Neither method is supported, do nothing
return;
}
// Logging code (uses hidden method to get substring with ^ at offset)
if (textNode?.nodeType === 3) {
const caretInText = getSubstringAroundOffset(textNode.textContent, offset);
log(
`node: ${textNode.nodeName}, offset: ${offset}, insert: ${caretInText}`,
);
}
// Only split TEXT_NODEs
if (textNode?.nodeType === 3) {
let replacement = textNode.splitText(offset);
let br = document.createElement("br");
textNode.parentNode.insertBefore(br, replacement);
}
}
The method is the added as the click event handler for any paragraph elements.
const paragraphs = document.getElementsByTagName("p");
for (const paragraph of paragraphs) {
paragraph.addEventListener("click", insertBreakAtPoint, false);
}
Results
Click anywhere in the Lorem ipsum ... paragraph below to insert a line break at the point where you click.
Note that the log shows the nodeName
, the offset, and a fragment of the selected node with a ^
character at the offset.
Split text nodes at caret positions in a Shadow DOM
This example demonstrates how to get the caret position from a selected node within a shadow root.
The example is very similar to the DOM-only example above, except that some of the text is inside a shadow root.
We provide a button to allow you to see the difference when a shadow root is passed/not passed to caretPositionFromPoint()
.
Note that some parts of the code are hidden, including code used for logging, as this is not useful for understanding this method.
HTML
The HTML defines a paragraph of text inside a <div>
element.
The paragraph contains a <span>
element with the id
of "host" that we will use as the host for a shadow root.
There are also some buttons that we'll use to reset the example, and to Add/Remove the shadow root option argument to caretPositionFromPoint()
.
<button id="reset" type="button">Reset</button>
<button id="shadowButton" type="button">Add Shadow</button>
<div>
<p>
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy
eirmod tempor invidunt ut <span id="host"></span> labore et dolore magna
aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo
dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est
Lorem ipsum dolor sit amet.
</p>
</div>
CSS
Here we use CSS to make the #host
element red and bold.
This makes it easier to distinguish between text in the DOM and text in the shadow DOM.
#host {
color: red;
font-weight: bold;
}
JavaScript
First we have some code to populate our shadow DOM.
We're using JavaScript to attach a shadow root dynamically, because the MDN example system does not allow us to do this declaratively using the <template>
element.
The content of the shadow DOM is a <span>
element that contains the text "I'm in the shadow DOM".
const host = document.querySelector("#host");
const shadow = host.attachShadow({ mode: "open" });
const shadowSpan = document.createElement("span");
shadowSpan.textContent = "I'm in the shadow DOM";
shadow.appendChild(shadowSpan);
Next we add a handler for our "Enable/Disable shadow" button.
This code toggles the value of the useShadows
variable and updates the button text appropriately.
let useShadows = false;
const shadowButton = document.querySelector("#shadowButton");
shadowButton.addEventListener("click", () => {
useShadows = !useShadows;
shadowButton.innerText = useShadows ? "Remove Shadow" : "Add Shadow";
});
The method below first checks for document.caretPositionFromPoint
support and uses it to get the text node and offset at the caret position.
The value of the useShadows
variable is used to determine whether the shadow root hosted in our text is passed to caretPositionFromPoint()
.
- If the browser doesn't support that method, the code then checks for
document.caretRangeFromPoint
, and uses that instead. - If the node at the caret position is a text node, the code then splits the node at the selected offset, and inserts a line break between them.
- If the node is an element node, then the code inserts a line break element node at the offset.
function insertBreakAtPoint(e) {
let range;
let textNode;
let offset;
if (document.caretPositionFromPoint) {
range = document.caretPositionFromPoint(
e.clientX,
e.clientY,
useShadows ? { shadowRoots: [shadow] } : null,
);
textNode = range.offsetNode;
offset = range.offset;
} else if (document.caretRangeFromPoint) {
// Use WebKit-proprietary fallback method
range = document.caretRangeFromPoint(e.clientX, e.clientY);
textNode = range.startContainer;
offset = range.startOffset;
} else {
// Neither method is supported, do nothing
return;
}
// Logging code (uses hidden method to get substring with ^ at offset)
if (textNode) {
if (textNode.nodeType === 3) {
const caretInText = getSubstringAroundOffset(
textNode.textContent,
offset,
);
log(
`type: TEXT_NODE, name: ${textNode.nodeName}, offset: ${offset}:
${caretInText}`,
);
} else if (textNode.nodeType === 1) {
log(`type: ELEMENT_NODE, name: ${textNode.nodeName}, offset: ${offset}`);
} else {
log(
`type: ${textNode.nodeType}, name: ${textNode.nodeName}, offset: ${offset}`,
);
}
}
// Insert line at caret
if (textNode?.nodeType === 3) {
// TEXT_NODE - split text at offset and add br
let replacement = textNode.splitText(offset);
let br = document.createElement("br");
textNode.parentNode.insertBefore(br, replacement);
} else if (textNode?.nodeType === 1) {
// ELEMENT_NODE - Add br node at offset node
let br = document.createElement("br");
const targetNode = textNode.childNodes[offset];
textNode.insertBefore(br, targetNode);
} else {
// Do nothing
}
}
Finally we add two click event handlers for paragraph elements in the DOM and in the shadow root, respectively.
Note that we need to specifically query the elements within the shadowRoot
as they are not visible to normal DOM query methods.
// Click event handler <p> elements in the DOM
const paragraphs = document.getElementsByTagName("p");
for (const paragraph of paragraphs) {
paragraph.addEventListener("click", insertBreakAtPoint, false);
}
// Click event handler <p> elements in the Shadow DOM
const shadowParagraphs = host.shadowRoot.querySelectorAll("p");
for (const paragraph of shadowParagraphs) {
console.log(paragraph);
paragraph.addEventListener("click", insertBreakAtPoint, false);
}
Results
Click in the Lorem ipsum ... paragraph before or after the shadow DOM text to insert a line break at the point where you click.
Note that in this case the log shows you have selected a TEXT_NODE
, the offset, and a fragment of the selected node with a ^
character at the offset.
Initially the shadow root is not passed to caretPositionFromPoint()
, so if you click on the text "I'm in the shadow DOM", the returned caret position node is the parent node of the host, at the offset of the shadow root.
The line break therefore gets added before the node rather than the point you selected.
Note that the caret position node in this case has the type ELEMENT_NODE
.
If you click the "Add shadow" button, the shadow root is passed to caretPositionFromPoint()
, so the returned caret position is the specific selected node within the shadow DOM.
This makes the shadow DOM text behave like the other paragraph text.
Specifications
Specification |
---|
CSSOM View Module # dom-document-caretpositionfrompoint |
Browser compatibility
BCD tables only load in the browser