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.
2 Answers
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

- 4,968
- 1
- 47
- 60
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.