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