39

I need the coordinates in pixels of the beginning of the text selection (anywhere on the page, not in a textarea).

I tried using the cursor coordinates but this didn't work quite well because the cursor coordinates and the beginning of the selection are not always the same (for example when a user drags over a text).

I hope someone has the solution!

General Grievance
  • 4,555
  • 31
  • 31
  • 45
Bouke
  • 660
  • 2
  • 7
  • 22

3 Answers3

69

In IE >= 9 and non-IE browsers (Firefox 4+, WebKit browsers released since early 2009, Opera 11, maybe earlier), you can use the getClientRects() method of Range. In IE 4 - 10, you can use the boundingLeft and boundingTop properties of the TextRange that can be extracted from the selection. Here's a function that will do what you want in recent browsers.

Note that there are some situations in which you may wrongly get co-ordinates 0, 0, as mentioned in the comments by @Louis. In that case you'll have to fall back to a workaround of temporarily inserting an element and getting its position.

jsFiddle: http://jsfiddle.net/NFJ9r/132/

Code:

function getSelectionCoords(win) {
    win = win || window;
    var doc = win.document;
    var sel = doc.selection, range, rects, rect;
    var x = 0, y = 0;
    if (sel) {
        if (sel.type != "Control") {
            range = sel.createRange();
            range.collapse(true);
            x = range.boundingLeft;
            y = range.boundingTop;
        }
    } else if (win.getSelection) {
        sel = win.getSelection();
        if (sel.rangeCount) {
            range = sel.getRangeAt(0).cloneRange();
            if (range.getClientRects) {
                range.collapse(true);
                rects = range.getClientRects();
                if (rects.length > 0) {
                    rect = rects[0];
                }
                x = rect.left;
                y = rect.top;
            }
            // Fall back to inserting a temporary element
            if (x == 0 && y == 0) {
                var span = doc.createElement("span");
                if (span.getClientRects) {
                    // Ensure span has dimensions and position by
                    // adding a zero-width space character
                    span.appendChild( doc.createTextNode("\u200b") );
                    range.insertNode(span);
                    rect = span.getClientRects()[0];
                    x = rect.left;
                    y = rect.top;
                    var spanParent = span.parentNode;
                    spanParent.removeChild(span);

                    // Glue any broken text nodes back together
                    spanParent.normalize();
                }
            }
        }
    }
    return { x: x, y: y };
}

UPDATE

I submitted a WebKit bug as a result of the comments, and it's now been fixed.

https://bugs.webkit.org/show_bug.cgi?id=65324

Tim Down
  • 318,141
  • 75
  • 454
  • 536
  • Works perfectly on single line selections, but when you select multiple lines (starting from somewhere half-where in the first line) it shows the coordinates for the beginning of the first selection-line not the beginning of the selection it self.... I know that is probably because of the getBounding function but is there any way to change that? – Bouke Jul 27 '11 at 18:02
  • @Bouke: Ah, fair point. I've updated my answer to do as you asked, by collapsing the range to a single point at the start before getting its position. – Tim Down Jul 27 '11 at 22:46
  • That works great on firefox, but now it doesn't seem to work on webkit browsers anymore... Seems like this is a webkit bug, maybe I need to use your older script as a fallback. – Bouke Jul 28 '11 at 11:41
  • @Bouke: You're right. WebKit's `getBoundingClientRect()` returns `null` for collapsed ranges, which is unhelpful. Working on it... – Tim Down Jul 28 '11 at 17:02
  • 1
    I have tried this code. It is returning always `x: 0, y:0` even the caret position is moved in firefox 20.0.1. Please have a look at this [similar question](http://stackoverflow.com/q/16212871/1577396) which I asked recently. I am looking for a cross-browser solution which works in IE8+, chrome and firefox. – Mr_Green May 20 '13 at 09:31
  • 1
    @Mr_Green: This code is specific to selections/ranges within regular HTML content rather than textareas, as is Rangy. I don't believe it is possible to create a general cross-browser solution for getting coordinates of the textarea cursor without doing lots of tedious text measuring, which is why I haven't answered your other question. – Tim Down May 20 '13 at 10:41
  • @TimDown: we might have done just that with the [component.io textarea-caret-position plugin](http://stackoverflow.com/a/22446703/1269037) - get the (x, y) position of `selectionStart`/`selectionEnd` in a textarea, cross-browser, and without using jQuery. – Dan Dascalescu Mar 17 '14 at 13:19
  • 1
    @TimDown There's more than an issue with textareas, I'm afraid. Run this [fiddle](http://jsfiddle.net/NFJ9r/) in Chrome and click to the left of "here" (which is in bold), without selecting text. You'll get `0, 0`. It is more easily reproducible in Chrome but the basic problem is that if a zero-width range is **not** in a text node, then the rectangle will have `0, 0` coordinates. I can't reproduce it in FF with a simple mouse click because it prefers to set the caret inside text nodes on clicks, but it could also happen in FF if the selection is manipulated programmatically. – Louis May 28 '14 at 19:28
  • @Louis: Yes, I've seen that too. The workaround is to temporarily insert an element and get its position. I'll add a note to the answer. – Tim Down May 28 '14 at 22:08
  • @TimDown there is an error when the caret is in an empty div – Jakobovski Oct 21 '14 at 19:50
  • Maybe add a comment on the `if (sel)` mentioning it targets only [some versions of IE](https://msdn.microsoft.com/en-us/library/ms535869(v=vs.85).aspx)? – Dan Dascalescu Jul 21 '15 at 07:17
  • @DanDascalescu This answer predates the existence of IE 11, which was the first not to support `document.selection`, but IE 11 does support Range and `Range.getClientRects` so there should be no problem with the actual code. I'll update the text though. Fair point about `rects[0]`, that's an oversight. – Tim Down Jul 21 '15 at 08:25
  • @TimDown thanks for this solution. You have solved all my problems. I don't know why, but it is not working correctly in complex html in iOS for example http://copytaste.com/aw8146 – Ankush Oct 04 '16 at 06:40
24

The above answer by TimDown does not work if the caret is in an empty element.

The code below solves the problem. Note how it is almost identical to TimDown's solution except that this code checks the range.getClientRects() array has length>0 before calling range.getClientRects()[0]

function getSelectionCoords() {
    var sel = document.selection, range, rect;
    var x = 0, y = 0;
    if (sel) {
        if (sel.type != "Control") {
            range = sel.createRange();
            range.collapse(true);
            x = range.boundingLeft;
            y = range.boundingTop;
        }
    } else if (window.getSelection) {
        sel = window.getSelection();
        if (sel.rangeCount) {
            range = sel.getRangeAt(0).cloneRange();
            if (range.getClientRects) {
                range.collapse(true);
                if (range.getClientRects().length>0){
                    rect = range.getClientRects()[0];
                    x = rect.left;
                    y = rect.top;
                }
            }
            // Fall back to inserting a temporary element
            if (x == 0 && y == 0) {
                var span = document.createElement("span");
                if (span.getClientRects) {
                    // Ensure span has dimensions and position by
                    // adding a zero-width space character
                    span.appendChild( document.createTextNode("\u200b") );
                    range.insertNode(span);
                    rect = span.getClientRects()[0];
                    x = rect.left;
                    y = rect.top;
                    var spanParent = span.parentNode;
                    spanParent.removeChild(span);

                    // Glue any broken text nodes back together
                    spanParent.normalize();
                }
            }
        }
    }
    return { x: x, y: y };
}
Jakobovski
  • 3,203
  • 1
  • 31
  • 38
6

The code below is a simplified and modernized version of the solution given by Tim Down. It also uses a more browser compatible selection API (window.getSelection() instead of window.document.selection)

type Coord = {
  x: number;
  y: number;
};

// atStart: if true, returns coord of the beginning of the selection,
//          if false, returns coord of the end of the selection
function getSelectionCoords(atStart: boolean): Coord | null {
  const sel = window.getSelection();

  // check if selection exists
  if (!sel.rangeCount) return null;

  // get range
  let range = sel.getRangeAt(0).cloneRange();
  if (!range.getClientRects) return null;

  // get client rect
  range.collapse(atStart);
  let rects = range.getClientRects();
  if (rects.length <= 0) return null;

  // return coord
  let rect = rects[0];
  return { x: rect.x, y: rect.y };
}
sujenp
  • 61
  • 1
  • 1