Compsoft Flexible Specialists

Compsoft plc

Compsoft Weblog Compsoft Website News Archive Privacy Policy Contact Us  

Wednesday, January 27, 2010

Using javascript to put the cursor at the end of a textbox

All too often with software development it's the feature that seems the simplest that takes the most time to implement.

We had a <textarea> on a page that typically contained a lot of text. The customer wanted that textarea to have the focus when they arrived at the page, with the cursor at the end of the text ready for them to type some more. If there's enough text in the box for the vertical scrollbar to appear, we'd need that to be scrolled to the bottom too.

"Sure, no problem!" I said, thinking this would take about five minutes to implement. It was more like two hours of research and wrestling with browser compatibility.

Let me save you two hours. Skip to the end and use my code, or stick around and share my pain.

The nicest way to position a cursor within a text input or textarea is to use the setSelectionRange function. This can also be used to select portions of text:

document.getElementById('myTextBox')
    .setSelectionRange(startPos, endPos);

Setting startPos and endPos to be the length of the text puts the cursor at the end. So far so good. Unfortunately, this is a non-standard javascript function, and some browsers doesn't support it - most notably IE.


Another method to put the cursor at the end is to replace the contents of the textbox with itself. Easy peasy with jQuery:

$(this).val($(this).val());

Unfortunately this doesn't work so well in Google Chrome: though it scrolls the textarea to the bottom, it leaves the cursor at the beginning of the text.


OK, so we use a combination of the two - use setSelectionRange if it's available, replace the text if not.


Now, though, we find that Firefox and Google Chrome won't have scrolled the textbox. That's OK - we can set the scrollTop property to a large value (a little dirty, but hey - it works):

this.scrollTop = 999999;

Phew! Job done, right?


Well no - there's still one little "unique browser feature" we have to account for, and not one you'd ever know about until you met it doing this sort of thing.


This bit of code should just work in all browsers that support setSelectionRange:

var len = $(this).val().length;
this.setSelectionRange(len, len);

If you're already saying "Ha! That'll never work in Opera!" then you're clearly too much of a cross-browser javascript-character-encoding genius to be reading this blog.


This will work fine if the textarea contains no carriage returns.


It turns out that when Opera measures strings (using .length), carriage returns count as one character. However, when it comes to setSelectionRange, they count as two.


This means that in Opera, the cursor will be positioned near the end, just not quite at the end - it'll be back by as many letters as there are carriage returns in total.


Ironically, Opera's perfectly capable of handling unicode characters just fine. You can mix Roman, Greek and Chinese characters in your textbox to your heart's content and Opera will count them consistently, no matter how many bytes each one takes to be stored. Press Enter though and all is lost.


This cross-culture check was an important one to make. If Opera had been counting characters for .length and counting bytes for setSelectionRange, things would have got a little complicated - you never know quite how many bytes will be needed to encode the most exotic character a user might one day need. As it's only carriage returns that mess us up, we know we can only ever be out by at most a factor of two. Thus we can just say this:

var len = $(this).val().length * 2;
this.setSelectionRange(len, len);

That will handle the worst case where the textbox contains only carriage returns.


A quick check to ensure that no browser minds us trying to go beyond the end of the string like this, and we really, truly, are all done now.


My only remaining moral obligation was to ensure that no one else had to reproduce this effort. I've bundled this all up into the PutCursorAtEnd jQuery plugin. The code for release 1.0 is as follows - tested in IE6, IE7, IE8, Firefox 3.5.5, Google Chrome 3.0, Safari 4.0.4, Opera 10.00, and probably safe in pretty much everything else too:

// jQuery plugin: PutCursorAtEnd 1.0
// http://plugins.jquery.com/project/PutCursorAtEnd
// by teedyay
//
// Puts the cursor at the end of a textbox/ textarea
// codesnippet: 691e18b1-f4f9-41b4-8fe8-bc8ee51b48d4
(function($)
{
    jQuery.fn.putCursorAtEnd = function()
    {
        return this.each(function()
        {
            $(this).focus()
            // If this function exists...
            if (this.setSelectionRange)
            {
                // ... then use it
                // (Doesn't work in IE)
                // Double the length because Opera is inconsistent about whether a carriage return is one character or two. Sigh.
                var len = $(this).val().length * 2;
                this.setSelectionRange(len, len);
            }
            else
            {
                // ... otherwise replace the contents with itself
                // (Doesn't work in Google Chrome)
                $(this).val($(this).val());
            }
            // Scroll to the bottom, in case we're in a tall textarea
            // (Necessary for Firefox and Google Chrome)
            this.scrollTop = 999999;
        });
    };
})(jQuery);

1 Comments:

At 11:02 pm, Blogger Matthew Flaschen said...

There is an inconsistency about whether the browser itself keeps carriage returns. Opera does, other browsers don't.

However, that alone doesn't seem to cause problems here, since it's internally consistent between .value and setSelectionRange.

I only saw an inconsistency when I used the jQuery val function. At the end (jQuery 1.4.2) of this function, there's the following code:

return (elem.value || "").replace(rreturn, "");

rreturn is /\r/g. In other words, jQuery is stripping the carriage returns. This causes inconsistencies in Opera.

jQuery may have made this decision due to some reasonable consideration. But here, I found the cleanest solution was to use .value directly.

 

Post a Comment

<< Home