Using JavaScript, I would like to create a collapsed range from a pixel position, in order to insert new nodes in the flow of the document, after the range identified by thi
An extension of Julien's answer above. This copes with multiple lines. Needs a little tweaking, but seems to work. It finds the number of lines by getting the height of a start to end selection, and the height of a single-letter selection, dividing the two, and rounding. There are probably situations where that won't work, but for most purposes...
function getLineCount(node, range) {
if ((node) && (range)) {
range.setStart(node, 0);
range.setEnd(node, 1);
var r = range.getBoundingClientRect();
var h1 = r.bottom - r.top;
range.setEnd(node, node.length);
r = range.getBoundingClientRect();
return Math.round((r.bottom - r.top) / h1);
}
};
Here's a tweaked version of the above code, using the line-count routine above. It also copes a little better with selections within the node, but off to the right of the actual text. None of this is optimized, but we're in user-time here, so milliseconds likely aren't too important.
function getSelectionNodeInfo(x, y) {
var startRange = document.createRange();
window.getSelection().removeAllRanges();
window.getSelection().addRange(startRange);
// Implementation note: range.setStart offset is
// counted in number of child elements if any or
// in characters if there is no childs. Since we
// want to compute in number of chars, we need to
// get the node which has no child.
var elem = document.elementFromPoint(x, y);
console.log("ElementFromPoint: " + $(elem).attr('class'));
var startNode = (elem.childNodes.length>0?elem.childNodes[0]:elem);
var lines = getLineCount(startNode, startRange);
console.log("Lines: " + lines);
var startCharIndexCharacter = 0;
startRange.setStart(startNode, 0);
startRange.setEnd(startNode, 1);
var letterCount = startNode.length;
var rangeRect = startRange.getBoundingClientRect();
var rangeWidth = 0
if (lines>1) {
while ((rangeRect.bottom < y) && (startCharIndexCharacter < (letterCount-1))) {
startCharIndexCharacter++;
startRange.setStart(startNode, startCharIndexCharacter);
startRange.setEnd(startNode, startCharIndexCharacter + 1);
rangeRect = startRange.getBoundingClientRect();
rangeWidth = rangeRect.right - rangeRect.left
}
}
while (rangeRect.left < (x-(rangeWidth/2)) && (startCharIndexCharacter < (letterCount))) {
startCharIndexCharacter++;
startRange.setStart(startNode, startCharIndexCharacter);
startRange.setEnd(startNode, startCharIndexCharacter + ((startCharIndexCharacter<letterCount) ? 1 : 0));
rangeRect = startRange.getBoundingClientRect();
rangeWidth = rangeRect.right - rangeRect.left
}
return {node:startNode, offsetInsideNode:startCharIndexCharacter};
}
Here is my implementation of caretRangeFromPoint
for old browsers:
if (!document.caretRangeFromPoint) {
document.caretRangeFromPoint = function(x, y) {
var log = "";
function inRect(x, y, rect) {
return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
}
function inObject(x, y, object) {
var rects = object.getClientRects();
for (var i = rects.length; i--;)
if (inRect(x, y, rects[i]))
return true;
return false;
}
function getTextNodes(node, x, y) {
if (!inObject(x, y, node))
return [];
var result = [];
node = node.firstChild;
while (node) {
if (node.nodeType == 3)
result.push(node);
if (node.nodeType == 1)
result = result.concat(getTextNodes(node, x, y));
node = node.nextSibling;
}
return result;
}
var element = document.elementFromPoint(x, y);
var nodes = getTextNodes(element, x, y);
if (!nodes.length)
return null;
var node = nodes[0];
var range = document.createRange();
range.setStart(node, 0);
range.setEnd(node, 1);
for (var i = nodes.length; i--;) {
var node = nodes[i],
text = node.nodeValue;
range = document.createRange();
range.setStart(node, 0);
range.setEnd(node, text.length);
if (!inObject(x, y, range))
continue;
for (var j = text.length; j--;) {
if (text.charCodeAt(j) <= 32)
continue;
range = document.createRange();
range.setStart(node, j);
range.setEnd(node, j + 1);
if (inObject(x, y, range)) {
range.setEnd(node, j);
return range;
}
}
}
return range;
};
}
Under MSIE, you wrote:
var range = document.selection.createRange();
range.moveToPoint(x, y);
For other browsers, the idea is to determine the HTML element at x/y position and to create a one character selection on it. Based on range.getBoundingClientRect()
, you can determine if the one character selection if before or after the x/y position. We then can select the next character until the selection position raich the x/y position. I wrote the following implementation for Firefox, Safari and Chrome:
var nodeInfo = getSelectionNodeInfo(x, y);
var range = document.createRange();
range.setStart(nodeInfo.node, nodeInfo.offsetInsideNode);
range.setEnd(nodeInfo.node, nodeInfo.offsetInsideNode);
/**
Emulates MSIE function range.moveToPoint(x,y) b
returning the selection node info corresponding
to the given x/y location.
@param x the point X coordinate
@param y the point Y coordinate
@return the node and offset in characters as
{node,offsetInsideNode} (e.g. can be passed to range.setStart)
*/
function getSelectionNodeInfo(x, y) {
var startRange = document.createRange();
window.getSelection().removeAllRanges();
window.getSelection().addRange(startRange);
// Implementation note: range.setStart offset is
// counted in number of child elements if any or
// in characters if there is no childs. Since we
// want to compute in number of chars, we need to
// get the node which has no child.
var elem = document.elementFromPoint(x, y);
var startNode = (elem.childNodes.length>0?elem.childNodes[0]:elem);
var startCharIndexCharacter = -1;
do {
startCharIndexCharacter++;
startRange.setStart(startNode, startCharIndexCharacter);
startRange.setEnd(startNode, startCharIndexCharacter+1);
var rangeRect = startRange.getBoundingClientRect();
} while (rangeRect.left<x && startCharIndexCharacter<startNode.length-1);
return {node:startNode, offsetInsideNode:startCharIndexCharacter};
}
These two piece of code have been tested under :
The following situations were not tested:
Here is the result of my investigation for getting a character position inside a text node from a pixel position:
The situation has changed since this question and most of the answers were posted: all major browsers now have at least one of the methods that make this relatively simple:
document.caretRangeFromPoint()
TextRange
object, which has a moveToPoint() method that takes pixel coordinates. However, it seems that moveToPoint()
can be buggy (see here and here, for example); I've simply been lucky that has worked in all the documents I've used it in.Note that in IE up to and including version 11, the object produced is a TextRange
rather than a DOM Range
. In versions of IE that support Range
, there is no easy way to convert between the two, although if you're willing to mess with the selection you can do something like the following, assuming you have a TextRange stored in a variable called textRange
:
textRange.select();
var range = window.getSelection().getRangeAt(0);
Here's some example code. It works in IE 5+, Edge, Safari and Chrome from around 2010 onwards, Firefox >= 20 and Opera >= 15.
Live demo: http://jsfiddle.net/timdown/rhgyw2dg/
Code:
function createCollapsedRangeFromPoint(x, y) {
var doc = document;
var position, range = null;
if (typeof doc.caretPositionFromPoint != "undefined") {
position = doc.caretPositionFromPoint(x, y);
range = doc.createRange();
range.setStart(position.offsetNode, position.offset);
range.collapse(true);
} else if (typeof doc.caretRangeFromPoint != "undefined") {
range = doc.caretRangeFromPoint(x, y);
} else if (typeof doc.body.createTextRange != "undefined") {
range = doc.body.createTextRange();
range.moveToPoint(x, y);
}
return range;
}