Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating a collapsed range from a pixel position in FF/Webkit

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 this position.

This can be done with the TextRange object in Internet Exporer (moveToPoint(x, y) method).

How can I do this in FireFox & Webkit?

I can get the container element from the position with document.elementFromPoint(x, y). But when the position happens to be inside a text node, how do I get more information about the text offset required to build a range?

like image 420
Yannick Deltroo Avatar asked Jul 06 '10 20:07

Yannick Deltroo


2 Answers

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;
    };
}
like image 67
Valentin Shergin Avatar answered Oct 28 '22 18:10

Valentin Shergin


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:

  • The standards-based approach from the CSSOM View spec: document.caretPositionFromPoint()
  • WebKit's proprietary version of the same: document.caretRangeFromPoint()
  • IE's proprietary 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;
}
like image 38
Tim Down Avatar answered Oct 28 '22 17:10

Tim Down