Thursday, May 4th, 2006

Handling Tabs in Textareas

Category: Examples, JavaScript

<>p>Over on bitprophet they have a test area for handling tabs in textareas.

The usecase for this is if you have an area and you want your users to be able to tab into it to enter text. Normally of course, you get tabbed out to the next form element.

This technique shouldn’t be used all over, but could be useful in niche applications. Of course if you do use this you should warn the user!

Handling the tab key in textarea

The Code

This example not only works with tab, but also overrides backspace, delete, and arrow keys.

javascript
< view plain text >
  1. // Set desired tab- defaults to four space softtab
  2. var tab = "    ";
  3.        
  4. function checkTab(evt) {
  5.     var t = evt.target;
  6.     var ss = t.selectionStart;
  7.     var se = t.selectionEnd;
  8.  
  9.     // Tab key - insert tab expansion
  10.     if (evt.keyCode == 9) {
  11.         evt.preventDefault();
  12.                
  13.         // Special case of multi line selection
  14.         if (ss != se && t.value.slice(ss,se).indexOf("\n") != -1) {
  15.             // In case selection was not of entire lines (e.g. selection begins in the middle of a line)
  16.             // we ought to tab at the beginning as well as at the start of every following line.
  17.             var pre = t.value.slice(0,ss);
  18.             var sel = t.value.slice(ss,se).replace(/\n/g,"\n"+tab);
  19.             var post = t.value.slice(se,t.value.length);
  20.             t.value = pre.concat(tab).concat(sel).concat(post);
  21.                    
  22.             t.selectionStart = ss + tab.length;
  23.             t.selectionEnd = se + tab.length;
  24.         }
  25.                
  26.         // "Normal" case (no selection or selection on one line only)
  27.         else {
  28.             t.value = t.value.slice(0,ss).concat(tab).concat(t.value.slice(ss,t.value.length));
  29.             if (ss == se) {
  30.                 t.selectionStart = t.selectionEnd = ss + tab.length;
  31.             }
  32.             else {
  33.                 t.selectionStart = ss + tab.length;
  34.                 t.selectionEnd = se + tab.length;
  35.             }
  36.         }
  37.     }
  38.            
  39.     // Backspace key - delete preceding tab expansion, if exists
  40.    else if (evt.keyCode==8 && t.value.slice(ss - 4,ss) == tab) {
  41.         evt.preventDefault();
  42.                
  43.         t.value = t.value.slice(0,ss - 4).concat(t.value.slice(ss,t.value.length));
  44.         t.selectionStart = t.selectionEnd = ss - tab.length;
  45.     }
  46.            
  47.     // Delete key - delete following tab expansion, if exists
  48.     else if (evt.keyCode==46 && t.value.slice(se,se + 4) == tab) {
  49.         evt.preventDefault();
  50.              
  51.         t.value = t.value.slice(0,ss).concat(t.value.slice(ss + 4,t.value.length));
  52.         t.selectionStart = t.selectionEnd = ss;
  53.     }
  54.     // Left/right arrow keys - move across the tab in one go
  55.     else if (evt.keyCode == 37 && t.value.slice(ss - 4,ss) == tab) {
  56.         evt.preventDefault();
  57.         t.selectionStart = t.selectionEnd = ss - 4;
  58.     }
  59.     else if (evt.keyCode == 39 && t.value.slice(ss,ss + 4) == tab) {
  60.         evt.preventDefault();
  61.         t.selectionStart = t.selectionEnd = ss + 4;
  62.     }
  63. }

Related Content:

23 Comments »

Comments feed TrackBack URI

Using a real tab (“\t”) seems better than simply inserting 4 spaces; it certainly requires a lot less magic. Also, how about changing the hardcoded 4 to tab.length so that you can actually adjust the number of spaces that are inserted without updating the rest of the code?

Comment by Matthias Miller — May 4, 2006

Using a real tab has an obvious accessibility downside: you can’t navigate between fields using the keyboard anymore.
The demo appears to support both “4 spaces -> tab” and “\t -> tab”, but if you try to navigate the form using your keyboard, you’ll see that you’re stuck in the textarea…

Comment by Julien Couvreur — May 4, 2006

I wonder how hard it would be to change it so that the textarea accepts Ctrl+Tab, or something like that? That way the expected behaviour would still be maintained and you get the tabs in the textarea as well.

Comment by Mike Ritchie — May 4, 2006

@ Mike: Actually if you use ctl-tab it moves you out of the text area and onto the next form element. It would be better to switch this behaviour and make ctl-tab the magic key to add a tab-space in your text and leave tab working as it is expected. All this method requires is a simple note telling users how to add a tab-space to their text.

Comment by Sean Fousheé — May 4, 2006

[...] Handle Tabs in Textareas with support for indenting selections etc. Very nice. (via) [...]

Pingback by Handling Tabs in Textareas at The Hero Dies in This One — May 4, 2006

If you do not wish to lose the TAB functionality, you can use SHIFT+TAB to insert a TAB just like you use SHIFT+ENTER to insert a new line on instant messengers like MSN Messenger.
Below is my small modification to make that happen.

NOTE: For IE, you can use the evt.shiftKey instead.

Change from

// Tab key – insert tab expansion
if (evt.keyCode == 9) {

to:


// Tab key - insert tab expansion
if ((evt.keyCode == 9)&&(evt.shiftKey)) {

Comment by Roy — May 4, 2006

Does anyone know if it’s possible to make this work in IE?

Comment by Darren — May 4, 2006


If you do not wish to lose the TAB functionality, you can use SHIFT+TAB to insert a TAB just like you use SHIFT+ENTER to insert a new line on instant messengers like MSN Messenger.

Except that Shift-Tab is already used to tab to the previous control. Control-Tab switches to next tab in FF, Ctrl-Shift-Tab switches to previous tab in FF, Alt-Tab and Alt-Shift-Tab similarly switch windows in Windows.

Ctrl-Alt-Tab and/or Ctrl-Alt-Shift-Tab might be available, but, uh, bleh.

Comment by Jeremy Dunck — May 5, 2006

theres a much better solution to all of this, onkeyup replace “\t” in that text field with an actual \t or ” ” (thats a real tab), you cant use modifiers like control shift or alt, they are already used, anyway this solution is way too bulky for something so simple (KISS)

Comment by Allen — May 5, 2006

[...] Ajaxian » Handling Tabs in Textareas (tags: javascript ajax tabs) [...]

Pingback by The Miles Rausch DootDoot Weblog Page Website By Awayken » Blog Archive » links for 2006-05-05 — May 5, 2006

I think this is very useful. I know that this is one of those things I will need someday and yet not want to have to spend time developing it. I have always been frusturated by the lack of tab support in the “online editors” that some blog apps (textpattern) provide. Im all about tabbing my code so it gets annoying to have to un-train my brain every time I have to bother with it. Now if we could only get someone to make this into a nice little textpattern or wordpress plugin…

Comment by Chris Iufer — May 5, 2006

> Does anyone know if it’s possible to make this work in IE?

I’ve got a working example somewhere. I’ll post it to my blog over the weekend.

Comment by Dean Edwards — May 5, 2006


In my browser CTRL-Tab already makes a tab. So, why do I need to use JavaScript to make the Tab key work?

Tabbed with CTRL-Tab (HTML doesn't show tabs.)

Comment by Phill Kenoyer — May 5, 2006

[JavaScript] Handling Tabs in Textareas

Trackback by Tucows Farm: The Tucows Developers' Hangout — May 8, 2006

Perhaps make a \t when pressing a lonnnnnng tab ?

Comment by Sunny — May 11, 2006

did you guy try this on IE? if you press any key a javascript error occurs

Comment by 50 Cent — June 29, 2006

I found this code very useful. Thanks! I modified the code to be cross-browser (firefox, safari, ie) and to handle only tab and shift-tab (for indent/outdent). I also made some modifications to make it work more like programs I’m used to. Anyway, don’t know if anyone is reading these messages anymore, but here’s my code if anyone finds it useful:


// place in the onkeypress event (onkeydown for ie!!) of a textarea
// t: textarea object, evt: event object
function enableTextareaTabInsertion(t, evt)
{
var kc = evt.which ? evt.which : evt.keyCode, isSafari = navigator.userAgent.toLowerCase().indexOf("safari") != -1;

if (kc == 9 || (isSafari && kc == 25))
{
t.focus();

// hack for ie
if (!t.selectionStart)
{
var range = document.selection.createRange();
var stored_range = range.duplicate();
stored_range.moveToElementText(t);
stored_range.setEndPoint('EndToEnd', range);
t.selectionStart = stored_range.text.length - range.text.length;
t.selectionEnd = t.selectionStart + range.text.length;
t.setSelectionRange = function(start, end)
{
var range = this.createTextRange();
range.collapse(true);
range.moveStart("character", start);
range.moveEnd("character", end - start);
range.select();
}
}

var tablen = 4, tab = ' ', tab_regexp = /\n\s\s\s\s/g;
var ss = t.selectionStart, se = t.selectionEnd, ta_val = t.value, sel = ta_val.slice(ss, se); shft = (isSafari && kc == 25) || evt.shiftKey;
var was_tab = ta_val.slice(ss - tablen, ss) == tab, starts_with_tab = ta_val.slice(ss, ss + tablen) == tab, offset = shft ? 0-tablen : tablen, full_indented_line = false, num_lines = sel.split("\n").length;

if (ss != se && sel[sel.length-1] == '\n') { se--; sel = ta_val.slice(ss, se); num_lines--; }
if (num_lines == 1 && starts_with_tab) full_indented_line = true;

if (!shft || was_tab || num_lines > 1 || full_indented_line)
{
// multi-line selection
if (num_lines > 1)
{
// tab each line
if (shft && (was_tab || starts_with_tab) && sel.split(tab_regexp).length == num_lines)
{
if (!was_tab) sel = sel.substring(tablen);
t.value = ta_val.slice(0, ss - (was_tab ? tablen : 0)).concat(sel.replace(tab_regexp, "\n")).concat(ta_val.slice(se, ta_val.length));
ss += was_tab ? offset : 0; se += offset * num_lines;
}
else if (!shft)
{
t.value = ta_val.slice(0, ss).concat(tab).concat(sel.replace(/\n/g, "\n" + tab)).concat(ta_val.slice(se, ta_val.length));
se += offset * num_lines;
}
}

// single-line selection
else
{
if (shft)
t.value = ta_val.slice(0, ss - (full_indented_line ? 0 : tablen)).concat(ta_val.slice(ss + (full_indented_line ? tablen : 0), ta_val.length));
else
t.value = ta_val.slice(0, ss).concat(tab).concat(ta_val.slice(ss, ta_val.length));

if (ss == se)
ss = se = ss + offset;
else
se += offset;
}
}

setTimeout("var t=$('" + t.id + "'); t.focus(); t.setSelectionRange(" + ss + ", " + se + ");", 0);
return false;
}
}

Comment by Matt White — September 26, 2006

I find that it makes life much easier if you add the following line:
var oldscroll = t.scrollTop;
at the beginning of the function; and add this line:
t.scrollTop = oldscroll;
at the end. Otherwise the textarea jumps with every tab.

Comment by Danny Mendel — May 8, 2007

This is perfect! Just what I was looking for!

Comment by Chris Beauchamp — July 31, 2007

Hey, I noticed that firefox, at least, was resetting the scroll position of the textarea whenever you interfere with its value. As such, it seems it’s best to cache the textarea’s scrollon line 9, and restore it on line 54.

I know that’s what I’m going.

Comment by Fordi — August 28, 2008

Also, a lot of those ‘-4′s should be replaced with tab.length

Comment by Fordi — August 28, 2008

This is a more cleaned up version of version of Greg’s from June 4th, 2007… Uses the onkeydown event to handle any textarea.

document.onkeydown = tabTextarea;

/**
* ORIGINAL SOURCE:
* http://www.answermysearches.com/here-is-how-to-make-the-tab-work-in-a-textarea-javascript/265/
* MODIFIED:
* 20090807 – Beall
* @param evt Native onkeydown event
*/
function tabTextarea(evt) {
// Get Event
var keyEvent = (evt || event);
// Get Key Code
var keyCode = keyEvent.keyCode;
// Get Event Target
var keyTarget = keyEvent.target ? keyEvent.target
: keyEvent.srcElement ? keyEvent.srcElement : null;

//TAB
if(keyCode==9 && keyTarget.tagName.toLowerCase() == “textarea”){
//Ctrl+Q – Used to not effect normal tab functionality
//if(keyEvent.ctrlKey && keyCode==81 && keyTarget.tagName.toLowerCase() == “textarea”){
//So the scroll won’t move after a tabbing
var oldscroll = keyTarget.scrollTop;

//Firefox
if (keyTarget.setSelectionRange) {
//Insert tab character
keyTarget.value = keyTarget.value.substring(0, keyTarget.selectionStart)
+ ‘\t’ + keyTarget.value.substring(keyTarget.selectionEnd,keyTarget.value.length);
}
//Handle IE
else {
//This much effort was almost unnecessary, but sometimes while
// tabbing, it would reset the cursor position to before the created tab
var textRange= document.selection.createRange();
textRange.text=’\t’;
var selectionLength = textRange.text.length;
textRange.moveStart (‘character’,-keyTarget.value.length);
var selectionStartPos = textRange.text.length-selectionLength;
var selectionEndPos = selectionStartPos+selectionLength;

if(selectionLength>1){
selectionLength=1;
selectionEndPos=selectionStartPos+1;
}

textRange = keyTarget.createTextRange();
textRange .collapse(true);
textRange .moveEnd(‘character’, selectionEndPos);
textRange .moveStart(‘character’, selectionEndPos);
textRange .select();
}

//put back the scroll
keyTarget.scrollTop = oldscroll;

keyEvent.cancelBubble = true;
return false;
}
}

Comment by devel — August 7, 2009

And few changes by me :D


enableTabs : function ( slectors ) {
jQuery( selectors ).focus(function () {
jQuery(this).keypress(function (e) {
var tab = "\t";
var target = e.target;
var selectionStart = target.selectionStart;
var selectionEnd = target.selectionEnd;

if (e.keyCode == 9) {
e.preventDefault();

// Multiline selection
if (selectionStart != selectionEnd && target.value.slice(selectionStart,selectionEnd).indexOf("\n") != -1) {
var pre = target.value.slice(0,selectionStart);
var sel = target.value.slice(selectionStart,selectionEnd);
var post = target.value.slice(selectionEnd,target.value.length);
if(sel.match(/\n(\t){1}/g))
var lines = sel.match(/\n(\t){1}/g).length + 1;
else
var lines = sel.split(/\n/).length;

if(!e.shiftKey) {
sel = sel.replace(/\n/g,"\n"+tab);
target.value = pre.concat(tab).concat(sel).concat(post);
target.selectionStart = selectionStart;
target.selectionEnd = selectionEnd + lines * tab.length;
} else {
if(sel.match(/^(\t){1}/m) || sel.match(/\n(\t){1}/g)) {
sel = sel.replace(/^(\t){1}/m,"");
sel = sel.replace(/\n(\t){1}/g,"\n");

target.value = pre.concat(sel).concat(post);
target.selectionStart = selectionStart;
target.selectionEnd = selectionEnd - lines * tab.length;
}
}
}
// No selection or sigle line selection
else {
if(!e.shiftKey) {
target.value = target.value.slice(0,selectionStart).concat(tab).concat(target.value.slice(selectionStart,target.value.length));
if (selectionStart == selectionEnd) {
target.selectionStart = target.selectionEnd = selectionStart + tab.length;
}
else {
target.selectionStart = selectionStart;
target.selectionEnd = selectionEnd + tab.length;
}
} else {
if (selectionStart != selectionEnd) {
var pre = target.value.slice(0,selectionStart);
var post = target.value.slice(selectionEnd,target.value.length);
var sel = target.value.slice(selectionStart,selectionEnd);

if(sel.match(/^(\t){1}/)) {
sel = sel.replace(/^(\t){1}/,"");
target.value = pre.concat(sel).concat(post);
target.selectionStart = selectionStart;
target.selectionEnd = selectionEnd - tab.length;
}
}
}
}
}
});
});
}

Useage :


enableTabs("textarea");

Comment by Jaromir — August 10, 2009

Leave a comment

You must be logged in to post a comment.