2

If I have a Range, I can get its bounding rectangle via getBoundingClientRect. Is the reverse possible? That is, given a rectangle of pixels create a Range.

paleozogt
  • 6,393
  • 11
  • 51
  • 94

2 Answers2

1

Yes and no.

This is not possible in the general case if you want to create only one Range, because the Range cannot select text in multiple locations in most browsers (e.g. if your "rectangle of pixels" height is more than one text line). Note that the "rectangle of pixels" you mention is more precisely a clipping rectangle.

But this is possible if you agree to have multiple Ranges. The main idea is to create one Range for each line of your clipping rectangle. The selection will not be shown on screen (because most browser do not support multiple selections), but at least you can extract the text which is in the clipping rectangle. This is how I implemented it:

var columnModeSelection = {
    startX:10,
    startY:10,
    endX:20,
    endY:30,
    selectedTextRanges:null // an Array of Range
};
createColumnModeSelection();

/**
Emulates MSIE function range.moveToPoint(x,y) by returning the selection node info which corresponds 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};
}

/**
Copy user selection to clipboard in plain text.
Multibrowser: supported under MSIE and WebKit
*/
function createColumnModeSelection() {
    // build a TextRange for each line to select
    var startY = columnModeSelection.startY;
    columnModeSelection.selectedTextRanges=new Array();
    while (startY<columnModeSelection.endY) {

        // select the line
        var range = null;
        if (document.selection) {
            // MSIE
            // Implementation note: the TextRange cannot be created from pixel 
            // coordinates, only the start point can. Thus, we are creating two 
            // TextRanges with a start point and set the end point of the first 
            // range to the start point of the end range.  

            // set the start point            
            range = document.selection.createRange();
            range.moveToPoint(columnModeSelection.startX, startY); 
            // set the end point         
            var endRange = document.selection.createRange();
            endRange.moveToPoint(columnModeSelection.endX, startY); // set the first line end
            range.setEndPoint("EndToStart", endRange);
            // create the selection
            range.select();
        } else {
            // other browsers
            var lineStartNodeInfo = getSelectionNodeInfo(columnModeSelection.startX, startY);
            var lineEndNodeInfo   = getSelectionNodeInfo(columnModeSelection.endX, startY);
            range = document.createRange();
            range.setStart(lineStartNodeInfo.node, lineStartNodeInfo.offsetInsideNode);
            range.setEnd(lineEndNodeInfo.node, lineEndNodeInfo.offsetInsideNode+1);
        }

        // keep the selection for later usage 
        if (range!=null) {
            columnModeSelection.selectedTextRanges.push(range);
        }

        // go to the next line
        var elem = document.elementFromPoint(columnModeSelection.startX, startY);
        var lineHeight = elem.clientHeight;
        startY += lineHeight;
    }

    // clear the last selected range
    if (document.selection) {
        // MSIE
        document.selection.empty();
    } else {
        // Safari, Firefox
        window.getSelection().removeAllRanges();
    }
}

This has been tested under the following browsers:

  • MSIE 7, MSIE 9
  • Firefox 5, Firefox 10
  • Chrome 9
  • Safari 5
Julien Kronegg
  • 4,968
  • 1
  • 47
  • 60
0

I've done this recently for selections. In browsers that support Range, it creates a range and then selects it, so it would be trivial to strip out the selection stuff.

Select from pixel coordinates for FF and Google Chrome

Community
  • 1
  • 1
Tim Down
  • 318,141
  • 75
  • 454
  • 536