12

I would like to position a floating div element in an iframe with contentEditable, in case the user enters a certain key combination (for auto-complete purposes).

I know how to get the caret position: document.getElementById('elm1_ifr').contentWindow.getSelection().anchorOffset

I can use this to calculate the left property of the div, but I can't seem to figure out how to get the top.

Another possibility I thought about was using: document.getElementById('elm1_ifr').contentWindow.getSelection().anchorNode.parentNode

And using jQuery to get the offset, but if that parent has a long text line, I would only be able to extract the top position of the first line.

Can anyone help me with this?

Rory McCrossan
  • 331,213
  • 40
  • 305
  • 339
Mor Shemesh
  • 2,689
  • 1
  • 24
  • 36

2 Answers2

13

The only reliable way of doing this is to insert a temporary element at the caret (ensuring that it is zero width), get its position and remove it again. You should also glue the two ends of the text node (if it was a text node that contained the caret) back together to ensure the DOM is as it was before inserting the node. Note, however, that doing this (or any other manual DOM manipulation on the editable content) breaks the browser's internal undo stack.

The reason for this is that careful reading of the spec for the getBoundingClientRect() method of Range shows that getBoundingClientRect() is not obliged to return a Rect for a collapsed Range. Conceptually, not every position in the document has a well-defined bounding rectangle. The caret, however, does have physical location on the screen which in my opinion should be provided by the Selection API, but currently there is nothing in browsers to provide this.

Tim Down
  • 318,141
  • 75
  • 454
  • 536
  • 2
    Couldn't you set the range to be from 0 to the current caret location, then use `getBoundingClientRect()` to get the left + width & top + height offsets, then restore the caret to it's previous setting? – Mottie Jun 19 '14 at 06:43
  • 3
    Old thread yes, but worth nting that inserting an element [using `execCommand('insertHTML')`] and removing it messes up the undo/redo stack. Doesn't matter if undo/redo is insignificant. – techfoobar Sep 15 '14 at 09:09
  • @techfoobar: Good point, thanks. I added a note to the answer. – Tim Down Sep 15 '14 at 14:10
9

I came into this problem today. After some testing, I got this working, without using temorary element.

In IE, it's easy to work it out with offsetLeft and offsetTop property of a TextRange object. Some effort is needed for webkit though.

Here's a test, you can see the result. http://jsfiddle.net/gliheng/vbucs/12/

var getCaretPixelPos = function ($node, offsetx, offsety){
    offsetx = offsetx || 0;
    offsety = offsety || 0;

    var nodeLeft = 0,
        nodeTop = 0;
    if ($node){
        nodeLeft = $node.offsetLeft;
        nodeTop = $node.offsetTop;
    }

    var pos = {left: 0, top: 0};

    if (document.selection){
        var range = document.selection.createRange();
        pos.left = range.offsetLeft + offsetx - nodeLeft + 'px';
        pos.top = range.offsetTop + offsety - nodeTop + 'px';
    }else if (window.getSelection){
        var sel = window.getSelection();
        var range = sel.getRangeAt(0).cloneRange();
        try{
            range.setStart(range.startContainer, range.startOffset-1);
        }catch(e){}
        var rect = range.getBoundingClientRect();
        if (range.endOffset == 0 || range.toString() === ''){
            // first char of line
            if (range.startContainer == $node){
                // empty div
                if (range.endOffset == 0){
                    pos.top = '0px';
                    pos.left = '0px';
                }else{
                    // firefox need this
                    var range2 = range.cloneRange();
                    range2.setStart(range2.startContainer, 0);
                    var rect2 = range2.getBoundingClientRect();
                    pos.left = rect2.left + offsetx - nodeLeft + 'px';
                    pos.top = rect2.top + rect2.height + offsety - nodeTop + 'px';
                }
            }else{
                pos.top = range.startContainer.offsetTop+'px';
                pos.left = range.startContainer.offsetLeft+'px';
            }
        }else{
            pos.left = rect.left + rect.width + offsetx - nodeLeft + 'px';
            pos.top = rect.top + offsety - nodeTop + 'px';
        }
    }
    return pos;
};
osamu
  • 983
  • 1
  • 9
  • 13
  • The problem with this, as I alluded to in my answer, is that `range.getBoundingClientRect()` in Firefox and other browsers sometimes return a Rect containing all zeroes for a collapsed range. See http://jsfiddle.net/CK3e8/ for one example. – Tim Down Oct 26 '12 at 12:57
  • That's true, tim. I tried to move the selection one character back, and work out the caret position from there. – osamu Oct 30 '12 at 10:58
  • 1
    Beautiful piece of code---thanks! I updated it in this fiddle to fix "undefinedpx" being returned if you moved to the first char of a line that already had text and to add a missing offset for empty divs: http://jsfiddle.net/kevinmicke/vbucs/262/ – kevinmicke Dec 29 '16 at 19:23