32

In my project I'm trying to get the offset position of the caret in a textarea in pixels. Can this be done?

Before asking here, I have gone through many links, especially Tim Down's, but I couldn't find a solution which works in IE8+, Chrome and Firefox. It seems Tim Down is working on this.

Some other links which I have found have many issues like not finding the top offset of the caret position.

I am trying to get the offset position of the caret because I want to show an auto-complete suggestion box inside the textarea by positioning it based on the offset position of the caret.

PS: I can't use a contenteditable div because I have written lots of code related to a textarea.

Community
  • 1
  • 1
Mr_Green
  • 40,727
  • 45
  • 159
  • 271
  • 1
    Have you tried this one: http://stackoverflow.com/a/1909997/2107024 The author says it works with IE. – MythThrazz Apr 25 '13 at 11:20
  • @MythThrazz If I am right, that will return index position of the caret in textarea not offset position of the caret. – Mr_Green Apr 25 '13 at 11:22
  • What's the difference between "offset position" and "index position"? – Aaron Digulla Apr 25 '13 at 12:36
  • @AaronDigulla using index position, I can get only the **left** value (based on width of letters, approximately) but not the **top** value. – Mr_Green Apr 25 '13 at 12:38
  • You want the X Y coordinates? – Tivie Apr 30 '13 at 22:56
  • Like line 1, column 7 – Tivie Apr 30 '13 at 23:04
  • @Tivie no, actually I want the offset position like `X: 45px, Y: 90px`. position of the caret with respect to the page or body or document or textarea. I think I will be ok with what you have proposed above. It would be nice if you give some demo as answer. – Mr_Green May 01 '13 at 05:29
  • 2
    @DuplicateVoters, this question has indeed been asked before, and at the moment it has an answer which probably addresses OP's needs quite well (I haven't tested this), but I'd like to point out that at the time when this question was asked and answered, the "question which has been asked before and already has an answer" did not already have an answer which addressed OP's need of getting the offset position of the caret in a textarea in pixels. – Mathijs Flietstra Apr 22 '14 at 09:45

5 Answers5

18

You can create a separate (invisible) element and fill it with textarea content from start to the cursor position. Textarea and the "clone" should have matching CSS (font properties, padding/margin/border and width). Then stack these elements on top of each other.

Let me start with a working example, then walk through the code: http://jsfiddle.net/g7rBk/

Updated Fiddle (with IE8 fix)

HTML:

<textarea id="input"></textarea>
<div id="output"><span></span></div>
<div id="xy"></div>

Textarea is self-explanatory. Output is a hidden element to which we'll pass text content and make measures. What's important is that we'll use an inline element. the "xy" div is just an indicator for testing purposes.

CSS:

/* identical styling to match the dimensions and position of textarea and its "clone"
*/
#input, #output {
    position:absolute;
    top:0;
    left:0;
    font:14px/1 monospace;
    padding:5px;
    border:1px solid #999;
    white-space:pre;
    margin:0;
    background:transparent;
    width:300px;
    max-width:300px;
}
/* make sure the textarea isn't obscured by clone */
#input { 
    z-index:2;
    min-height:200px;
}

#output { 
    border-color:transparent; 
}

/* hide the span visually using opacity (not display:none), so it's still measurable; make it break long words inside like textarea does. */
#output span {
    opacity:0;
    word-wrap: break-word;
    overflow-wrap: break-word;
}
/* the cursor position indicator */
#xy { 
    position:absolute; 
    width:4px;
    height:4px;
    background:#f00;
}

JavaScript:

/* get references to DOM nodes we'll use */
var input = document.getElementById('input'),
    output = document.getElementById('output').firstChild,
    position = document.getElementById('position'),

/* And finally, here it goes: */
    update = function(){
         /* Fill the clone with textarea content from start to the position of the caret. You may need to expand here to support older IE [1]. The replace /\n$/ is necessary to get position when cursor is at the beginning of empty new line.
          */
         output.innerHTML = input.value.substr( 0, input.selectionStart ).replace(/\n$/,"\n\001");

        /* the fun part! 
           We use an inline element, so getClientRects[2] will return a collection of rectangles wrapping each line of text.
           We only need the position of the last rectangle.
         */
        var rects = output.getClientRects(),
            lastRect = rects[ rects.length - 1 ],
            top = lastRect.top - input.scrollTop,
            left = lastRect.left+lastRect.width;
        /* position the little div and see if it matches caret position :) */
        xy.style.cssText = "top: "+top+"px;left: "+left+"px";
    }

[1] Caret position in textarea, in characters from the start

[2] https://developer.mozilla.org/en/docs/DOM/element.getClientRects

Edit: This example only works for fixed-width textarea. To make it work with user-resizable textarea you'd need to add an event listener to the resize event and set the #output dimensions to match new #input dimensions.

Community
  • 1
  • 1
pawel
  • 35,827
  • 7
  • 56
  • 53
16

Here's an approach using rangyinputs, rangy and jQuery.

It basically copies the whole text from inside the textarea into a div of the same size. I have set some CSS to ensure that in every browser, the textarea and the div wrap their content in exactly the same way.

When the textarea is clicked, I read out at which character index the caret is positioned, then I insert a caret span at the same index inside the div. By only doing that I ended up having an issue with the caret span jumping back to the previous line if the user clicked at the start of a line. To fix that I check if the previous character is a space (which would allow a wrap to occur), if that is true, I wrap it in a span, and I wrap the next word (the one directly after the caret position) in a span. Now I compare the top values between these two span's, if they differ, there was some wrapping going on, so I assume that the top and the left value of the #nextword span are equivalent to the caret position.

This approach can still be improved upon, I'm sure I haven't thought of everything that could possibly go wrong, and even if I have, then I haven't bothered implementing a fix for all of them as I don't have the time to do so at the moment, a number of things that you would need to look at:

  • it doesn't yet handle hard returns inserted with Enter (fixed)
  • positioning breaks when entering multiple spaces in a row (fixed)
  • I think hyphens would allow a content wrap to occur as well..

Currently it works exactly the same way across browsers here on Windows 8 with the latest versions of Chrome, Firefox, IE and Safari. My testing has not been very rigorous though.

Here's a jsFiddle.

I hope it will help you, at the very least it might give you some ideas to build on.

Some Features:

  • I have included a ul for you which is positioned in the right spot, and fixed a Firefox issue where the textarea selection was not re-set back to its original spot after the DOM manipulations.

  • I have added IE7 - IE9 support and fixed the multiple word selection issue pointed out in the comments.

  • I have added support for hard returns inserted with Enter and multiple spaces in a row.

  • I have fixed an issue with the default behaviour for the ctrl+shift+left arrow text selection method.

JavaScript

function getTextAreaXandY() {

    // Don't do anything if key pressed is left arrow
    if (e.which == 37) return;     

    // Save selection start
    var selection = $(this).getSelection();
    var index = selection.start;

    // Copy text to div
    $(this).blur();
    $("div").text($(this).val());

    // Get current character
    $(this).setSelection(index, index + 1);
    currentcharacter = $(this).getSelection().text;

    // Get previous character
    $(this).setSelection(index - 1, index)
    previouscharacter = $(this).getSelection().text;

    var start, endchar;
    var end = 0;
    var range = rangy.createRange();

    // If current or previous character is a space or a line break, find the next word and wrap it in a span
    var linebreak = previouscharacter.match(/(\r\n|\n|\r)/gm) == undefined ? false : true;
    
    if (previouscharacter == ' ' || currentcharacter == ' ' || linebreak) {
        i = index + 1; // Start at the end of the current space        
        while (endchar != ' ' && end < $(this).val().length) {
            i++;
            $(this).setSelection(i, i + 1)
            var sel = $(this).getSelection();
            endchar = sel.text;
            end = sel.start;
        }

        range.setStart($("div")[0].childNodes[0], index);
        range.setEnd($("div")[0].childNodes[0], end);
        var nextword = range.toHtml();
        range.deleteContents();
        var position = $("<span id='nextword'>" + nextword + "</span>")[0];
        range.insertNode(position);
        var nextwordtop = $("#nextword").position().top;
    }

    // Insert `#caret` at the position of the caret
    range.setStart($("div")[0].childNodes[0], index);
    var caret = $("<span id='caret'></span>")[0];
    range.insertNode(caret);
    var carettop = $("#caret").position().top;

    // If preceding character is a space, wrap it in a span
    if (previouscharacter == ' ') {
        range.setStart($("div")[0].childNodes[0], index - 1);
        range.setEnd($("div")[0].childNodes[0], index);
        var prevchar = $("<span id='prevchar'></span>")[0];
        range.insertNode(prevchar);
        var prevchartop = $("#prevchar").position().top;
    }

    // Set textarea selection back to selection start
    $(this).focus();
    $(this).setSelection(index, selection.end);

    // If the top value of the previous character span is not equal to the top value of the next word,
    // there must have been some wrapping going on, the previous character was a space, so the wrapping
    // would have occured after this space, its safe to assume that the left and top value of `#nextword`
    // indicate the caret position
    if (prevchartop != undefined && prevchartop != nextwordtop) {
        $("label").text('X: ' + $("#nextword").position().left + 'px, Y: ' + $("#nextword").position().top);
        $('ul').css('left', ($("#nextword").position().left) + 'px');
        $('ul').css('top', ($("#nextword").position().top + 13) + 'px');
    }
    // if not, then there was no wrapping, we can take the left and the top value from `#caret`    
    else {
        $("label").text('X: ' + $("#caret").position().left + 'px, Y: ' + $("#caret").position().top);
        $('ul').css('left', ($("#caret").position().left) + 'px');
        $('ul').css('top', ($("#caret").position().top + 14) + 'px');
    }

    $('ul').css('display', 'block');
}

$("textarea").click(getTextAreaXandY);
$("textarea").keyup(getTextAreaXandY);

HTML

<div></div>
<textarea>Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.</textarea>
<label></label>
<ul>
    <li>Why don't you type this..</li>
</ul>

CSS

body {
    font-family: Verdana;
    font-size: 12px;
    line-height: 14px;
}
textarea, div {
    font-family: Verdana;
    font-size: 12px;
    line-height: 14px;
    width: 300px;
    display: block;
    overflow: hidden;
    border: 1px solid black;
    padding: 0;
    margin: 0;
    resize: none;
    min-height: 300px;
    position: absolute;
    -moz-box-sizing: border-box;
    white-space: pre-wrap;
}
span {
    display: inline-block;
    height: 14px;
    position: relative;
}
span#caret {
    display: inline;
}
label {
    display: block;
    margin-left: 320px;
}
ul {
    padding: 0px;
    margin: 9px;
    position: absolute;
    z-index: 999;
    border: 1px solid #000;
    background-color: #FFF;
    list-style-type:none;
    display: none;
}
@media screen and (-webkit-min-device-pixel-ratio:0) {
    span {
        white-space: pre-wrap;
    }
}
div {
    /* Firefox wrapping fix */
    -moz-padding-end: 1.5px;
    -moz-padding-start: 1.5px;
    /* IE8/IE9 wrapping fix */
    padding-right: 5px\0/;
    width: 295px\0/;
}
span#caret
{
    display: inline-block\0/;
}
Community
  • 1
  • 1
Mathijs Flietstra
  • 12,900
  • 3
  • 38
  • 67
7

There's a much simpler solution for getting the caret position in pixels, than what's been presented in the other answers.

Note that this question is a duplicate of a 2008 one, and I've answered it here. I'll only maintain the answer at that link, since this question should have been closed as duplicate years ago.

Copy of the answer

I've looked for a textarea caret coordinates plugin for meteor-autocomplete, so I've evaluated all the 8 plugins on GitHub. The winner is, by far, textarea-caret-position from Component.

Features

  • pixel precision
  • no dependencies whatsoever
  • browser compatibility: Chrome, Safari, Firefox (despite two bugs it has), IE9+; may work but not tested in Opera, IE8 or older
  • supports any font family and size, as well as text-transforms
  • the text area can have arbitrary padding or borders
  • not confused by horizontal or vertical scrollbars in the textarea
  • supports hard returns, tabs (except on IE) and consecutive spaces in the text
  • correct position on lines longer than the columns in the text area
  • no "ghost" position in the empty space at the end of a line when wrapping long words

Here's a demo - http://jsfiddle.net/dandv/aFPA7/

enter image description here

How it works

A mirror <div> is created off-screen and styled exactly like the <textarea>. Then, the text of the textarea up to the caret is copied into the div and a <span> is inserted right after it. Then, the text content of the span is set to the remainder of the text in the textarea, in order to faithfully reproduce the wrapping in the faux div.

This is the only method guaranteed to handle all the edge cases pertaining to wrapping long lines. It's also used by GitHub to determine the position of its @ user dropdown.

Community
  • 1
  • 1
Dan Dascalescu
  • 143,271
  • 52
  • 317
  • 404
2

JsFiddle of working example: http://jsfiddle.net/42zHC/2/

Basically, we figure out how many columns fit in the width (since it will be monospace). We have to force scrollbars to always be there otherwise the calculation is off. Then we divide the number of columns that fit with the width, and we get the x offset per character. Then we set the line height on the textarea. Since we know how many characters are in a row, we can divide that with the number of characters and we get the row number. With the line height, we now have the y offset. Then we get the scrollTop of the textarea and subtract that, so that once it starts using the scrollbar, it still shows up in the right position.

Javascript:

$(document).ready(function () {
  var cols = document.getElementById('t').cols;
  var width = document.getElementById('t').clientWidth;
  var height = $('textarea').css('line-height');
  var pos = $('textarea').position();
  $('#t').on('keyup', function () {
    el = document.getElementById("t");
    if (el.selectionStart) { 
        selection = el.selectionStart; 
      } else if (document.selection) { 
        el.focus(); 
        var r = document.selection.createRange(); 
        if (r == null) { 
           selection = 0; 
        } 
        var re = el.createTextRange(), 
        rc = re.duplicate(); 
        re.moveToBookmark(r.getBookmark()); 
        rc.setEndPoint('EndToStart', re); 
        selection = rc.text.length; 
      } else { selection = 0 }
    var row = Math.floor((selection-1) / cols);
    var col = (selection - (row * cols));
    var x = Math.floor((col*(width/cols)));
    var y = (parseInt(height)*row);
    $('span').html("row: " + row + "<br>columns" + col + "<br>width: " + width + "<br>x: " + x +"px<br>y: " + y +"px<br>Scrolltop: "+$(this).scrollTop()).css('top',pos.top+y-$(this).scrollTop()).css('left',pos.left+x+10);
  });
});

HTML:

<textarea id="t"></textarea>
<br>
<span id="tooltip" style="background:yellow"></span>

CSS:

textarea {
  height: 80px;
  line-height: 12px;
  overflow-y:scroll;
}
span {
  position: absolute;
}
dave
  • 62,300
  • 5
  • 72
  • 93
  • Thanks for your response. It is cool but has some limitations like it doesn't work when I press enter and type something. :) – Mr_Green May 17 '13 at 12:09
  • Depending on the font being monospace is a pretty big, and needless limitation. @Mr_Green, I've looked at all the [textarea caret position](http://stackoverflow.com/a/22446703/1269037) plugins on GitHub, and improved the most capable, robust and simple one. It has no dependencies, works on Firefox, Chrome and IE9 (possibly in older browsers as well), and it's only 80 lines of code. – Dan Dascalescu Mar 17 '14 at 13:15
  • @DanDascalescu Thanks.. Somehow I didn't get notification of your ping. The project is completed so I am not testing it.. I hope it is helpful for others :). BTW, you can check in olders browsers using jsfiddle by adding `show`. something like `jsfiddle.net/abcd/show`.. – Mr_Green May 14 '14 at 04:58
-1

I couldn't get something similar to work, so my solution was to locate the character position of the caret in the textarea, cut out the current paragraph and display this next to the textarea.

Using the offset, I placed a fake cursor (div, display:inline, 1px wide, border-left: 1px solid black) in this view of the editable text.

This way, you can create a visual feedback area where you can show the result of effects (quite like stackoverflow does when you write an answer).

Aaron Digulla
  • 321,842
  • 108
  • 597
  • 820
  • 1
    Sorry, I can't understand your explanation. – Mr_Green May 15 '13 at 06:59
  • It's a workaround. Instead of trying to find the cursor position, I show the user what the text would look like. But whether this is useful depends on why you need to know the caret position. – Aaron Digulla May 15 '13 at 14:14
  • Because I am trying to show a suggestion box made of ul element and position it according to those offset position values. – Mr_Green May 15 '13 at 17:56