Coordinates of selected text in browser page
Asked Answered
D

3

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!

Dirigible answered 27/7, 2011 at 14:38 Comment(0)
T
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

Ticklish answered 27/7, 2011 at 15:48 Comment(13)
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?Dirigible
@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.Ticklish
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.Dirigible
@Bouke: You're right. WebKit's getBoundingClientRect() returns null for collapsed ranges, which is unhelpful. Working on it...Ticklish
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 which I asked recently. I am looking for a cross-browser solution which works in IE8+, chrome and firefox.Barvick
@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.Ticklish
@TimDown: we might have done just that with the component.io textarea-caret-position plugin - get the (x, y) position of selectionStart/selectionEnd in a textarea, cross-browser, and without using jQuery.Shrieval
@TimDown There's more than an issue with textareas, I'm afraid. Run this fiddle 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.Merbromin
@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.Ticklish
@TimDown there is an error when the caret is in an empty divSst
Maybe add a comment on the if (sel) mentioning it targets only some versions of IE?Shrieval
@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.Ticklish
@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 copytaste.com/aw8146Culicid
S
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 };
}
Sst answered 21/10, 2014 at 19:56 Comment(2)
Good point. Do you mind if I include that check in my answer?Ticklish
@TimDown Sure you can include it in your answer.Sst
B
7

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 };
}
Brownlee answered 13/3, 2021 at 20:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.