Thursday, May 4th, 2006
Handling Tabs in Textareas
<>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!
The Code
This example not only works with tab, but also overrides backspace, delete, and arrow keys.
-
-
// Set desired tab- defaults to four space softtab
-
var tab = " ";
-
-
function checkTab(evt) {
-
var t = evt.target;
-
var ss = t.selectionStart;
-
var se = t.selectionEnd;
-
-
// Tab key - insert tab expansion
-
if (evt.keyCode == 9) {
-
evt.preventDefault();
-
-
// Special case of multi line selection
-
if (ss != se && t.value.slice(ss,se).indexOf("n") != -1) {
-
// In case selection was not of entire lines (e.g. selection begins in the middle of a line)
-
// we ought to tab at the beginning as well as at the start of every following line.
-
var pre = t.value.slice(0,ss);
-
var sel = t.value.slice(ss,se).replace(/n/g,"n"+tab);
-
var post = t.value.slice(se,t.value.length);
-
t.value = pre.concat(tab).concat(sel).concat(post);
-
-
t.selectionStart = ss + tab.length;
-
t.selectionEnd = se + tab.length;
-
}
-
-
// "Normal" case (no selection or selection on one line only)
-
else {
-
t.value = t.value.slice(0,ss).concat(tab).concat(t.value.slice(ss,t.value.length));
-
if (ss == se) {
-
t.selectionStart = t.selectionEnd = ss + tab.length;
-
}
-
else {
-
t.selectionStart = ss + tab.length;
-
t.selectionEnd = se + tab.length;
-
}
-
}
-
}
-
-
// Backspace key - delete preceding tab expansion, if exists
-
else if (evt.keyCode==8 && t.value.slice(ss - 4,ss) == tab) {
-
evt.preventDefault();
-
-
t.value = t.value.slice(0,ss - 4).concat(t.value.slice(ss,t.value.length));
-
t.selectionStart = t.selectionEnd = ss - tab.length;
-
}
-
-
// Delete key - delete following tab expansion, if exists
-
else if (evt.keyCode==46 && t.value.slice(se,se + 4) == tab) {
-
evt.preventDefault();
-
-
t.value = t.value.slice(0,ss).concat(t.value.slice(ss + 4,t.value.length));
-
t.selectionStart = t.selectionEnd = ss;
-
}
-
// Left/right arrow keys - move across the tab in one go
-
else if (evt.keyCode == 37 && t.value.slice(ss - 4,ss) == tab) {
-
evt.preventDefault();
-
t.selectionStart = t.selectionEnd = ss - 4;
-
}
-
else if (evt.keyCode == 39 && t.value.slice(ss,ss + 4) == tab) {
-
evt.preventDefault();
-
t.selectionStart = t.selectionEnd = ss + 4;
-
}
-
}
-









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?
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…
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.
@ 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.
[...] Handle Tabs in Textareas with support for indenting selections etc. Very nice. (via) [...]
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)) {
Does anyone know if it’s possible to make this work in IE?
”
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.
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)
[...] Ajaxian » Handling Tabs in Textareas (tags: javascript ajax tabs) [...]
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…
> 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.
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.)
[JavaScript] Handling Tabs in Textareas
Perhaps make a \t when pressing a lonnnnnng tab ?
did you guy try this on IE? if you press any key a javascript error occurs
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;
}
}
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.
This is perfect! Just what I was looking for!
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.
Also, a lot of those ‘-4′s should be replaced with tab.length
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;
}
}
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");