194

I have a simple HTML textarea on my site.

Right now, if you click Tab in it, it goes to the next field. I would like to make the tab button indent a few spaces instead.

How can I do this?

janw
  • 8,758
  • 11
  • 40
  • 62
user780483
  • 2,903
  • 10
  • 29
  • 32

31 Answers31

183

Borrowing heavily from other answers for similar questions (posted below)...

document.getElementById('textbox').addEventListener('keydown', function(e) {
  if (e.key == 'Tab') {
    e.preventDefault();
    var start = this.selectionStart;
    var end = this.selectionEnd;

    // set textarea value to: text before caret + tab + text after caret
    this.value = this.value.substring(0, start) +
      "\t" + this.value.substring(end);

    // put caret at right position again
    this.selectionStart =
      this.selectionEnd = start + 1;
  }
});
<input type="text" name="test1" />
<textarea id="textbox" name="test2"></textarea>
<input type="text" name="test3" />

jQuery: How to capture the TAB keypress within a Textbox

How to handle <tab> in textarea?

Cristian Ciupitu
  • 20,270
  • 7
  • 50
  • 76
kasdega
  • 18,396
  • 12
  • 45
  • 89
62
var textareas = document.getElementsByTagName('textarea');
var count = textareas.length;
for(var i=0;i<count;i++){
    textareas[i].onkeydown = function(e){
        if(e.keyCode==9 || e.which==9){
            e.preventDefault();
            var s = this.selectionStart;
            this.value = this.value.substring(0,this.selectionStart) + "\t" + this.value.substring(this.selectionEnd);
            this.selectionEnd = s+1; 
        }
    }
}

This solution does not require jQuery and will enable tab functionality on all textareas on a page.

Yaroslav Sivakov
  • 470
  • 3
  • 14
user1949974
  • 637
  • 5
  • 3
  • Can you get this to work with 4 spaces instead of a \t? If you replace \t with " " it will insert the spaces but leave the caret behind. – Sinaesthetic Jan 18 '14 at 07:29
  • 1
    @Sinaesthetic: yes, you may change tab to spaces, but you have to adapt a bit the code (there is 3-4 new letters instead of one). The other alternative is the CSS tab-size. – Adrian Maire Apr 06 '15 at 09:26
  • 1
    @Sinaesthetic Yes, simply replace last line `this.selectionEnd = s+1;` with `this.selectionEnd = s + "\t".length;`. It would be cleaner to use a variable or function parameter and store the indentation char(s) there. But you know what to replace now: The `+1` defines how much chars the caret is moved. – StanE Oct 13 '16 at 17:13
  • 3
    `KeyboardEvent.keyCode` and `KeyboardEvent.which` are deprecated properties. Use [`KeyboardEvent.key`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key) instead. – Константин Ван Nov 19 '16 at 06:31
  • @StanE - I used your suggestion on the tab character and now the single tab character occupies a width of approx 6 space characters, which is what I wanted - when you use cursor keys to navigate through the text, the caret now jumps that tab distance on a single key press. The tab is removed with a single backspace too. Just noting here for anyone else who wants tab to look like a tab character too. – youcantryreachingme Nov 08 '20 at 22:46
58

As others have written, you can use JavaScript to capture the event, prevent the default action (so that the cursor does not shift focus) and insert a tab character.

But, disabling the default behavior makes it impossible to move the focus out of the text area without using a mouse. Blind users interact with web pages using the keyboard and nothing else -- they can't see the mouse pointer to do anything useful with it, so it's keyboard or nothing. The tab key is the primary way to navigate the document, and especially forms. Overriding the default behavior of the tab key will make it impossible for blind users to move the focus to the next form element.

So, if you're writing a web site for a broad audience, I'd recommend against doing this without a compelling reason, and provide some kind of alternative for blind users that doesn't trap them in the textarea.

Will Martin
  • 4,142
  • 1
  • 27
  • 38
  • 2
    thank you. I dont mean to sound rud , but i didnt know blind people used computers. I will keep this in mind – user780483 Jul 10 '11 at 14:44
  • 14
    That's okay, a lot of people don't know; it's just outside their experience. Here's an introduction: http://webaim.org/intro/ – Will Martin Jul 10 '11 at 18:37
  • 1
    Yeah really bad idea if this is a website for general audiences. In addition to screen reader users, there are many other users who for various reasons either must, or chose to, navigate with the keyboard. Trapping the tab key will make the form at least annoying and quite possibly unusable for these users. – steveax Aug 23 '13 at 02:39
  • 6
    Maybe use control + tab. This will highjack the browsers ability to other tabs(webpages), but users can just tab out of the textbox and then control tab to the other page. Should have a hint on the page use ctrl + tab for tab. – Joseph McIntyre Oct 20 '13 at 20:02
  • 1
    Thanks @WillMartin Very valuable piece of information. I was going to implement the same thing in my whole blog for all textarea without considering this very crucial point. – Imran Dec 28 '16 at 15:25
53

For what it's worth, here's my oneliner, for what you all have been talking about in this thread:

<textarea onkeydown="if(event.keyCode===9){var v=this.value,s=this.selectionStart,e=this.selectionEnd;this.value=v.substring(0, s)+'\t'+v.substring(e);this.selectionStart=this.selectionEnd=s+1;return false;}">
</textarea>

Testest in latest editions of Chrome, Firefox, Internet Explorer and Edge.

elgholm
  • 638
  • 5
  • 8
34

Here's my version of this, supports:

  • tab + shift tab
  • maintains undo stack for simple tab character inserts
  • supports block line indent/unindent but trashes undo stack
  • properly selects whole lines when block indent/unindent
  • supports auto indent on pressing enter (maintains undo stack)
  • use Escape key cancel support on next tab/enter key (so you can press Escape then tab out)
  • Works on Chrome + Edge, untested others.

$(function() { 
 var enabled = true;
 $("textarea.tabSupport").keydown(function(e) {

  // Escape key toggles tab on/off
  if (e.keyCode==27)
  {
   enabled = !enabled;
   return false;
  }

  // Enter Key?
  if (e.keyCode === 13 && enabled)
  {
   // selection?
   if (this.selectionStart == this.selectionEnd)
   {
    // find start of the current line
    var sel = this.selectionStart;
    var text = $(this).val();
    while (sel > 0 && text[sel-1] != '\n')
    sel--;

    var lineStart = sel;
    while (text[sel] == ' ' || text[sel]=='\t')
    sel++;

    if (sel > lineStart)
    {
     // Insert carriage return and indented text
     document.execCommand('insertText', false, "\n" + text.substr(lineStart, sel-lineStart));

     // Scroll caret visible
     this.blur();
     this.focus();
     return false;
    }
   }
  }

  // Tab key?
  if(e.keyCode === 9 && enabled) 
  {
   // selection?
   if (this.selectionStart == this.selectionEnd)
   {
    // These single character operations are undoable
    if (!e.shiftKey)
    {
     document.execCommand('insertText', false, "\t");
    }
    else
    {
     var text = this.value;
     if (this.selectionStart > 0 && text[this.selectionStart-1]=='\t')
     {
      document.execCommand('delete');
     }
    }
   }
   else
   {
    // Block indent/unindent trashes undo stack.
    // Select whole lines
    var selStart = this.selectionStart;
    var selEnd = this.selectionEnd;
    var text = $(this).val();
    while (selStart > 0 && text[selStart-1] != '\n')
     selStart--;
    while (selEnd > 0 && text[selEnd-1]!='\n' && selEnd < text.length)
     selEnd++;

    // Get selected text
    var lines = text.substr(selStart, selEnd - selStart).split('\n');

    // Insert tabs
    for (var i=0; i<lines.length; i++)
    {
     // Don't indent last line if cursor at start of line
     if (i==lines.length-1 && lines[i].length==0)
      continue;

     // Tab or Shift+Tab?
     if (e.shiftKey)
     {
      if (lines[i].startsWith('\t'))
       lines[i] = lines[i].substr(1);
      else if (lines[i].startsWith("    "))
       lines[i] = lines[i].substr(4);
     }
     else
      lines[i] = "\t" + lines[i];
    }
    lines = lines.join('\n');

    // Update the text area
    this.value = text.substr(0, selStart) + lines + text.substr(selEnd);
    this.selectionStart = selStart;
    this.selectionEnd = selStart + lines.length; 
   }

   return false;
  }

  enabled = true;
  return true;
 });
});
textarea
{
  width: 100%;
  height: 100px;
  tab-size: 4;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<textarea class="tabSupport">if (something)
{
 // This textarea has "tabSupport" CSS style
 // Try using tab key
 // Try selecting multiple lines and using tab and shift+tab
 // Try pressing enter at end of this line for auto indent
 // Use Escape key to toggle tab support on/off
 //     eg: press Escape then Tab to go to next field
}
</textarea>
<textarea>This text area doesn't have tabSupport class so disabled here</textarea>
Jamon Holmgren
  • 23,738
  • 6
  • 59
  • 75
Brad Robinson
  • 44,114
  • 19
  • 59
  • 88
  • 4
    This is the best answer here. – fourk Oct 04 '18 at 18:14
  • 1
    This works without jQuery with a little bit of work. Check youmightnotneedjquery.com for help. Definitely the best answer here, too. – Jamon Holmgren May 09 '19 at 18:48
  • 2
    @JamonHolmgren Everything can work without JQuery: http://vanilla-js.com/ – Jack G Aug 20 '20 at 18:57
  • 2
    I don't know why this is not the accepted answer, its very elegant. I needed to use it in TypeScript class (without jQuerY) so I thought I would share my class as it may help others: https://jsfiddle.net/2wkrhxLt/8/ – gcoulby Feb 03 '21 at 17:20
16

Modern way that both is straight-forward and does not lose the ability to undo (Ctrl+Z) the last changes.

$('#your-textarea').keydown(function (e) {
    var keyCode = e.keyCode || e.which;

    if (keyCode === $.ui.keyCode.TAB) {
        e.preventDefault();

        const TAB_SIZE = 4;

        // The one-liner that does the magic
        document.execCommand('insertText', false, ' '.repeat(TAB_SIZE));
    }
});

More about execCommand:


Edit:

As pointed out in the comment (and while this was once a "modern" solution), the feature has gone obsolete. Quoting the docs:

This feature is obsolete. Although it may still work in some browsers, its use is discouraged since it could be removed at any time. Try to avoid using it.

Yom T.
  • 8,760
  • 2
  • 32
  • 49
  • 3
    This is the only correct answer at this point. Huge thank you. – Chris Calo Dec 17 '18 at 22:34
  • 2
    Sadly no Firefox support. Try [`indent-textarea`](https://github.com/bfred-it/indent-textarea) for a cross-browser solution that uses this method + a fallback in Firefox. – fregante Apr 12 '19 at 09:40
  • In Firefox, `document.execCommand` only becomes [enabled](https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand) after setting `document.designMode = "on";`. I'm able to get text to write into elements that have `.contentEditable = 'true'`. However, when I try to accomplish this on a textarea, the inserted textNode gets placed right before the textarea within the document (instead of into the textarea). Please try to help Mozilla figure this out [here](https://bugzilla.mozilla.org/show_bug.cgi?id=1220696). – Lonnie Best Nov 16 '19 at 19:19
  • Be aware, this is no longer considered 'modern', the page you linked notes (about `execCommand`): 'This feature is obsolete. Although it may still work in some browsers, its use is discouraged since it could be removed at any time. Try to avoid using it.' – kasimir May 08 '20 at 15:27
  • ```nativeElement.addEventListener("keydown", function (e) { if (e.key === "Tab") { e.preventDefault(); const TAB_SIZE = 4; document.execCommand('insertText', false, ' '.repeat(TAB_SIZE)); } })``` – Said Torres Oct 18 '22 at 20:34
9

I was getting nowhere fast trying to use @kasdega's answer in an AngularJS environment, nothing I tried seemed able to make Angular act on the change. So in case it's of any use to passers by, here's a rewrite of @kasdega's code, AngularJS style, which worked for me:

app.directive('ngAllowTab', function () {
    return function (scope, element, attrs) {
        element.bind('keydown', function (event) {
            if (event.which == 9) {
                event.preventDefault();
                var start = this.selectionStart;
                var end = this.selectionEnd;
                element.val(element.val().substring(0, start) 
                    + '\t' + element.val().substring(end));
                this.selectionStart = this.selectionEnd = start + 1;
                element.triggerHandler('change');
            }
        });
    };
});

and:

<textarea ng-model="mytext" ng-allow-tab></textarea>
stovroz
  • 6,835
  • 2
  • 48
  • 59
  • It is very important to call `element.triggerHandler('change');`, otherwise the model won't be updated (because of the `element.triggerHandler('change');` I think. – Alvaro Flaño Larrondo Sep 16 '16 at 14:11
9

This solution allows tabbing in an entire selection like your typical code editor, and untabbing that selection too. However, I haven't figured out how to implement shift-tab when there's no selection.

$('#txtInput').on('keydown', function(ev) {
    var keyCode = ev.keyCode || ev.which;

    if (keyCode == 9) {
        ev.preventDefault();
        var start = this.selectionStart;
        var end = this.selectionEnd;
        var val = this.value;
        var selected = val.substring(start, end);
        var re, count;

        if(ev.shiftKey) {
            re = /^\t/gm;
            count = -selected.match(re).length;
            this.value = val.substring(0, start) + selected.replace(re, '') + val.substring(end);
            // todo: add support for shift-tabbing without a selection
        } else {
            re = /^/gm;
            count = selected.match(re).length;
            this.value = val.substring(0, start) + selected.replace(re, '\t') + val.substring(end);
        }

        if(start === end) {
            this.selectionStart = end + count;
        } else {
            this.selectionStart = start;
        }

        this.selectionEnd = end + count;
    }
});
#txtInput {
  font-family: monospace;
  width: 100%;
  box-sizing: border-box;
  height: 200px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>


<textarea id="txtInput">
$(document).ready(function(){
 $("#msgid").html("This is Hello World by JQuery");
});
</textarea>
mpen
  • 272,448
  • 266
  • 850
  • 1,236
  • This works good, but you could at least limit the shift-tab with no selection from throwing errors. I did it with simple `if (selected.length > 0) {...}` Fiddle: https://jsfiddle.net/jwfkbjkr –  Jun 20 '17 at 15:58
6

Multiple-line indetation script based on @kasdega solution.

$('textarea').on('keydown', function (e) {
    var keyCode = e.keyCode || e.which;

    if (keyCode === 9) {
        e.preventDefault();
        var start = this.selectionStart;
        var end = this.selectionEnd;
        var val = this.value;
        var selected = val.substring(start, end);
        var re = /^/gm;
        var count = selected.match(re).length;


        this.value = val.substring(0, start) + selected.replace(re, '\t') + val.substring(end);
        this.selectionStart = start;
        this.selectionEnd = end + count;
    }
});
Martin
  • 440
  • 4
  • 5
  • 1
    Best solution so far, but it probably shouldn't create a selection when `start === end`. – mpen Feb 12 '15 at 17:07
6

You have to write JS code to catch TAB key press and insert a bunch of spaces. Something similar to what JSFiddle does.

Check jquery fiddle:

HTML:

<textarea id="mybox">this is a test</textarea>

JavaScript:

$('#mybox').live('keydown', function(e) { 
  var keyCode = e.keyCode || e.which; 

  if (keyCode == 9) { 
    e.preventDefault(); 
    alert('tab pressed');
  } 
});
​
Aziz Shaikh
  • 16,245
  • 11
  • 62
  • 79
  • 2
    Don't forget to prevent the default action as well. `event.preventDefault();` – Ryan Jul 09 '11 at 20:54
  • 2
    Live has been replaced with on in newer versions. – Eric Harms Oct 09 '13 at 16:06
  • [The event.which property normalizes event.keyCode and event.charCode](https://api.jquery.com/event.which/). You shouldn't need to check `e.keyCode || e.which`. – Trevor Oct 29 '15 at 16:01
4

Hold ALT and press 0,9 from numeric keypad. It works in google-chrome

krishna
  • 59
  • 1
  • Nice, never thought yet I use `alt+92` for \ and `alt+124` for | since I don't have them on keyboard, now I'll use alt+09 for tab, thanks. This is [ASCII code](https://www.ascii-code.com) for who is wondering. – Martzy Apr 29 '22 at 14:17
4

The simplest way I found to do that in modern browsers with vanilla JavaScript is:

  <textarea name="codebox"></textarea>
  
  <script>
  const codebox = document.querySelector("[name=codebox]")

  codebox.addEventListener("keydown", (e) => {
    let { keyCode } = e;
    let { value, selectionStart, selectionEnd } = codebox;

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

      codebox.value = value.slice(0, selectionStart) + "\t" + value.slice(selectionEnd);

      codebox.setSelectionRange(selectionStart+2, selectionStart+2)
    }
  });
  </script>

Note that I used many ES6 features in this snippet for the sake of simplicity, you'll probably want to transpile it (with Babel or TypeScript) before deploying it.

birgersp
  • 3,909
  • 8
  • 39
  • 79
Telmo Trooper
  • 4,993
  • 1
  • 30
  • 35
3

Based on all that people had to say here on the answers, its just a combination of keydown(not keyup) + preventDefault() + insert a tab character at the caret. Something like:

var keyCode = e.keyCode || e.which;
if (keyCode == 9) {
   e.preventDefault();
   insertAtCaret('txt', '\t')
}

The earlier answer had a working jsfiddle but it used an alert() on keydown. If you remove this alert, then it didnt work. I ve just added a function to insert a tab at the current cursor position in the textarea.

Here s a working jsfiddle for the same: http://jsfiddle.net/nsHGZ/

walmik
  • 1,440
  • 2
  • 13
  • 30
3

I see this subject is not solved. I coded this and it's working very well. It insert a tabulation at the cursor index. Without using jquery

<textarea id="myArea"></textarea>
<script>
document.getElementById("myArea").addEventListener("keydown",function(event){
    if(event.code==="Tab"){
        var cIndex=this.selectionStart;
        this.value=[this.value.slice(0,cIndex),//Slice at cursor index
            "\t",                              //Add Tab
            this.value.slice(cIndex)].join('');//Join with the end
        event.stopPropagation();
        event.preventDefault();                //Don't quit the area
        this.selectionStart=cIndex+1;
        this.selectionEnd=cIndex+1;            //Keep the cursor in the right index
    }
});
</script>
Bibimission
  • 317
  • 5
  • 17
  • 2
    This code messes up the undo stack, though. After use insert a tab, sometimes you can't undo anything or just 1-2 steps back. – M. Eriksson Dec 06 '18 at 14:30
2

The above answers all wipe undo history. For anyone looking for a solution that doesn't do that, I spent the last hour coding up the following for Chrome:

jQuery.fn.enableTabs = function(TAB_TEXT){
    // options
    if(!TAB_TEXT)TAB_TEXT = '\t';
    // text input event for character insertion
    function insertText(el, text){
        var te = document.createEvent('TextEvent');
        te.initTextEvent('textInput', true, true, null, text, 9, "en-US");
        el.dispatchEvent(te);
    }
    // catch tab and filter selection
    jQuery(this).keydown(function(e){
        if((e.which || e.keyCode)!=9)return true;
        e.preventDefault();
        var contents = this.value,
            sel_start = this.selectionStart,
            sel_end = this.selectionEnd,
            sel_contents_before = contents.substring(0, sel_start),
            first_line_start_search = sel_contents_before.lastIndexOf('\n'),
            first_line_start = first_line_start_search==-1 ? 0 : first_line_start_search+1,
            tab_sel_contents = contents.substring(first_line_start, sel_end),
            tab_sel_contents_find = (e.shiftKey?new RegExp('\n'+TAB_TEXT, 'g'):new RegExp('\n', 'g')),
            tab_sel_contents_replace = (e.shiftKey?'\n':'\n'+TAB_TEXT);
            tab_sel_contents_replaced = (('\n'+tab_sel_contents)
                .replace(tab_sel_contents_find, tab_sel_contents_replace))
                .substring(1),
            sel_end_new = first_line_start+tab_sel_contents_replaced.length;
        this.setSelectionRange(first_line_start, sel_end);
        insertText(this, tab_sel_contents_replaced);
        this.setSelectionRange(first_line_start, sel_end_new);
    });
};

In short, tabs are inserted at the beginning of the selected lines.

JSFiddle: http://jsfiddle.net/iausallc/5Lnabspr/11/

Gist: https://gist.github.com/iautomation/e53647be326cb7d7112d

Example usage: $('textarea').enableTabs('\t')

Cons: Only works on Chrome as is.

iautomation
  • 996
  • 10
  • 18
  • What part of this script is only working in Chrome? Is it the "TextEvent"? http://help.dottoro.com/lagstsiq.php/#TextEvent This site says it should also work in IE9+ and Safari. Since I need this for a Chrome App this is perfect. – Andreas Linnert Sep 08 '15 at 09:23
  • @Andreas Linnert you are right. It is documented to work in both IE and Safari. However, at the time of this writing IE did not work for me, and I simply didn't have time to look into it further, and I had not tested in Safari. Apologies for the confusion. I'm glad to have helped. – iautomation Sep 08 '15 at 12:20
2

I made one that you can access with any textarea element you like:

function textControl (element, event)
{
    if(event.keyCode==9 || event.which==9)
    {
        event.preventDefault();
        var s = element.selectionStart;
        element.value = element.value.substring(0,element.selectionStart) + "\t" + element.value.substring(element.selectionEnd);
        element.selectionEnd = s+1; 
    }
}

And the element would look like this:

<textarea onkeydown="textControl(this,event)"></textarea>
2

Here's a simple pure-JS approach that supports basic indenting and dedenting.

Unfortunately, it doesn't preserve the undo history or support block-level tabbing.

document.querySelectorAll('textarea').forEach(function(textarea)
{
    textarea.onkeydown = function(e)
    {
        if (e.keyCode === 9 || e.which === 9)
        {
            e.preventDefault();
            if (e.shiftKey && this.selectionStart)
            {
                if (this.value[this.selectionStart -1] === "\t")
                {
                    var s = this.selectionStart;
                    this.value = this.value.substring(0,this.selectionStart - 1) + this.value.substring(this.selectionEnd);
                    this.selectionEnd = s-1; 
                }
          
            }
            
            if (!e.shiftKey)
            {
                var s = this.selectionStart;
                this.value = this.value.substring(0,this.selectionStart) + "\t" + this.value.substring(this.selectionEnd);
                this.selectionEnd = s+1; 
            }
        }
    }
});
Pikamander2
  • 7,332
  • 3
  • 48
  • 69
1

You can use the setRangeText() method available on the textarea element to do this natively.

HTML

<textarea id='my-textarea' onkeydown="handleKeyDown(event)"></textarea>

JS

const handleKeyDown = e => {
if (e.key === 'Tab') {
    e.preventDefault();
    const textArea = e.currentTarget; // or use document.querySelector('#my-textarea');
    textArea.setRangeText(
      '\t',
      textArea.selectionStart,
      textArea.selectionEnd,
      'end'
    );
  }
};

setRangeText is used for replacing text, but since we only want to insert a \t, we simply set the selection to the start and end of the current selection. The 'end' value tells the method to move the cursor to the end of the inserted text.

Bonus CSS

If you want to change the tab size, you can use the tab-size property on block elements. The default for most browsers is 8.

textarea {
  tab-size: 4;
}

Mozilla: HTMLInputElement.setRangeText()

Mozzila: Tab-Size

Josh Weston
  • 1,632
  • 22
  • 23
1
document.querySelector('textarea').addEventListener("keydown", function (e) {
  if (e.key == "Tab") {
   e.preventDefault();
   let start = this.selectionStart;
   let end = this.selectionEnd;
   // set textarea value to: text before caret + tab + text after caret
   this.value =
   this.value.substring(0, start) + "\t" + this.value.substring(end);
  // put caret at right position again
  this.selectionStart = this.selectionEnd = start + 1;
 }
});
0

There is a library on Github for tab support in your textareas by wjbryant: Tab Override

This is how it works:

// get all the textarea elements on the page
var textareas = document.getElementsByTagName('textarea');

// enable Tab Override for all textareas
tabOverride.set(textareas);
Pho3nixHun
  • 820
  • 2
  • 11
  • 24
  • Not a bad answer, but looking at the code it uses pretty much the same techniques as the ones described in some of the answers here: https://github.com/wjbryant/taboverride/blob/master/src/taboverride.js#L284. This means that it doesn't preserve the undo history, which is the main issue. – mihai Mar 19 '20 at 16:54
0

Every input an textarea element has a onkeydown event. In the event handler you can prevent the default reaction of the tab key by using event.preventDefault() whenever event.keyCode is 9.

Then put a tab sign in the right position:

function allowTab(input)
{
    input.addEventListener("keydown", function(event)
    {
        if(event.keyCode == 9)
        {
            event.preventDefault();

            var input = event.target;

            var str = input.value;
            var _selectionStart = input.selectionStart;
            var _selectionEnd = input.selectionEnd;

            str = str.substring(0, _selectionStart) + "\t" + str.substring(_selectionEnd, str.length);
            _selectionStart++;

            input.value = str;
            input.selectionStart = _selectionStart;
            input.selectionEnd = _selectionStart;
        }
    });
}

window.addEventListener("load", function(event)
{
    allowTab(document.querySelector("textarea"));
});

html

<textarea></textarea>
Martin Wantke
  • 4,287
  • 33
  • 21
0

Simple standalone script:

textarea_enable_tab_indent = function(textarea) {    
    textarea.onkeydown = function(e) {
        if (e.keyCode == 9 || e.which == 9){
            e.preventDefault();
            var oldStart = this.selectionStart;
            var before   = this.value.substring(0, this.selectionStart);
            var selected = this.value.substring(this.selectionStart, this.selectionEnd);
            var after    = this.value.substring(this.selectionEnd);
            this.value = before + "    " + selected + after;
            this.selectionEnd = oldStart + 4;
        }
    }
}
kungfooman
  • 4,473
  • 1
  • 44
  • 33
0

As an option to kasdega's code above, instead of appending the tab to the current value, you can instead insert characters at the current cursor point. This has the benefit of:

  • allows you to insert 4 spaces as an alternative to tab
  • undo and redo will work with the inserted characters (it won't with the OP)

so replace

    // set textarea value to: text before caret + tab + text after caret
    $(this).val($(this).val().substring(0, start)
                + "\t"
                + $(this).val().substring(end));

with

    // set textarea value to: text before caret + tab + text after caret
    document.execCommand("insertText", false, '    ');
dyg
  • 11
  • 2
0

I've tried some solutions and none of them worked, so I came up with this:

document.addEventListener('keydown', (e) => {
    if (e.code === 'Tab') {
        e.preventDefault();

        const TAB_WIDTH = 4;

        //* Apply 1 space for every tab width
        document.execCommand('insertText', false, ' '.repeat(TAB_WIDTH));
    }
});
0

I don't have a good enough reputation to post a comment, or I would have added this as a comment to Brad Robinson's answer, as a follow-on to gcoulby's comment.

I was inspired by both. Result is available as a fiddle: https://jsfiddle.net/qmyh76tu/1/

... but also as a snippet in this answer, since I can't post a fiddle without also posting code.

This version does a few extra things, most notably:

  • fully preserves the undo stack
  • implemented in pure JS

EDIT 2022-10-29: Small change to snippet and fiddle to fix an issue I'd found when trying to outdent with the cursor at the beginning of the line. Also added an extra textarea for comparison without the use of tab_editor().

EDIT 2022-10-30: Another small change to fix an issue with auto-indent, and to add home/end bouncing.

// Found this:
//    https://stackoverflow.com/questions/6637341/use-tab-to-indent-in-textarea
//  ... then this:
//    https://jsfiddle.net/2wkrhxLt/8/
//  ... then this:
//    https://extendsclass.com/typescript-to-javascript.html
//  Now works with more than one textarea, and fully preserves the undo
//  stack.  Behaviour closely matches common text editors like Pluma:
//    - obeys computed tab-size style attribute
//    - inserts when Tab key used without selection
//    - can be configured to insert spaces instead of tabs
//      - obeys tab positions i.e. modulo tab-size
//    - block indenting
//    - outdents with SHIFT-Tab key (with or without selection)
//    - auto-indents
//    - Home/End bouncing
//    - preserves selection/cursor
//    - scrolls to cursor or selection start

// Begin enabled.
var tab_editor_enabled = true;

function tab_editor(target, use_spaces)
{
  // Abort if other modifier keys are pressed.
  if (event.ctrlKey || event.altKey)
    { return; }

  // Preserve original selection points.
  original_start = target.selectionStart;
  original_end   = target.selectionEnd;

  // Prepare.
  selection_start = original_start;
  selection_end   = original_end;
  selection = (selection_start != selection_end);
  text = target.value;
  tab_sz = window.getComputedStyle(target).tabSize;
  next_enabled_state = true;

  // Key handler.
  switch (event.key)
  {
    // Esc restores default Tab functionality i.e. move to next field.
    // Acts as a toggle so an even-number of ESC recaptures the Tab key.
    case 'Escape':
      event.preventDefault();
      tab_editor_enabled = !tab_editor_enabled;
      next_enabled_state = false;
      break;

    // Shift by itself preserves enabled state so that a prior Esc also
    // restores default SHIFT-Tab functionality i.e. move to previous field.
    case 'Shift':
      next_enabled_state = tab_editor_enabled;
      break;

    // Auto-indent.
    case 'Enter':
      // Only without selection.
      if (!selection)
      {
        // Find start of the current line.
        while ((selection_start > 0) && (text[selection_start-1] != '\n'))
          { selection_start--; }
        line_start = selection_start;
        // Find first non-whitespace character.
        while ((text[selection_start] == ' ') || (text[selection_start] == '\t'))
          { selection_start++; }
        // If those two aren't the same, insert whitespace to auto-indent.
        if (selection_start != line_start)
        {
          event.preventDefault();
          // Insert newline and indented text.
          insert = '\n' + text.substr(line_start, Math.min(original_start, selection_start) - line_start);
          document.execCommand('insertText', false, insert);
        }
      }
      // Scroll to make caret visible
      target.blur();
      target.focus();
      break;

    // Bounce home.
    case 'Home':
      // Find start of the current line.
      while ((selection_start > 0) && (text[selection_start-1] != '\n'))
        { selection_start--; }

      // If cursor was already there, bounce to indent.
      if (selection_start == original_start)
      {
        event.preventDefault();
        // Find first non-whitespace character.
        while ((text[selection_start] == ' ') || (text[selection_start] == '\t'))
          { selection_start++; }
        if (event.shiftKey)
        {
          target.selectionStart = selection_start <= selection_end
                                ? selection_start
                                : selection_end;
          target.selectionEnd   = selection_start <= selection_end
                                ? selection_end
                                : selection_start;
        }
        else
        {
          target.selectionStart = selection_start;
          target.selectionEnd   = selection_start;
        }
      }
      // Scroll to make caret visible
      target.blur();
      target.focus();
      break;

    // Bounce end.
    case 'End':
      // Find end of the current line.
      while ((text[selection_end] != '\n') && (selection_end < text.length))
        { selection_end++; }
      //selection_end--;

      // If cursor was already there, bounce to last non-whitespace character.
      if (selection_end == original_end)
      {
        event.preventDefault();
        // Find last non-whitespace character.
        while ((text[selection_end-1] == ' ') || (text[selection_end-1] == '\t'))
          { selection_end--; }
        if (event.shiftKey)
        {
          target.selectionStart = selection_start <= selection_end
                                ? selection_start
                                : selection_end;
          target.selectionEnd   = selection_start <= selection_end
                                ? selection_end
                                : selection_start;
        }
        else
        {
          target.selectionStart = selection_end;
          target.selectionEnd   = selection_end;
        }
      }
      // Scroll to make caret visible
      target.blur();
      target.focus();
      break;

    // Tab with or without SHIFT modifier key.
    case 'Tab':

      // Previously disabled by Esc, so break without capturing key.
      if (!tab_editor_enabled)
        { break; }

      // Capture Tab key.
      event.preventDefault();

      // Insert or remove (indent or outdent).
      remove = event.shiftKey;

      // No selection, inserting.
      if (!selection && !remove)
      {
        // If using spaces, compute how many we need to add based on caret
        // relative to beginning of line, and any tab characters which may
        // already be there.
        if (use_spaces)
        {
          while ((selection_start > 0) && (text[selection_start-1] != '\n'))
            { selection_start--; }
          pos = 0;
          while (selection_start < original_start)
          {
            pos += text[selection_start] == '\t'
                 ? tab_sz - (pos % tab_sz)
                 : 1;
            selection_start++;
          }
          insert = ' '.repeat(tab_sz - (pos % tab_sz));
        }
        else
        {
          insert = '\t';
        }
        // Insert and move cursor.
        document.execCommand('insertText', false, insert);
        original_start += insert.length;
        original_end   += insert.length;
      }

      // With selection, or no selection but outdenting.
      else
      {
        // Moves backwards from start of selection, and stops when:
        //    - reach start of textarea
        //    - reached beginning of line
        while ((selection_start > 0) && (text[selection_start-1] != '\n'))
          { selection_start--; }

        // Start of first line.  Used to anchor the cursor when outdenting
        // without a selection.
        first_line_start = selection_start;

        // Moves forwards from end of selection, and stops when:
        //    - reach end of textarea
        //    - reached the beginning of the next line.
        selection_end = Math.max(selection_end, selection_start + 1);
        selection_end = Math.min(selection_end, text.length - 1);
        while ((text[selection_end-1] != '\n') && (selection_end < text.length))
          { selection_end++; }

        // We now have an array of full lines without trailing newlines.
        lines = text.substr(selection_start, (selection_end - selection_start)).split('\n');

        // Insert/remove tabs/spaces on each line.
        for (n=0; n<lines.length; n++)
        {
          // Don't indent last line if cursor at start of line.
          if ((n == (lines.length - 1)) && (lines[n].length == 0))
            { continue; }

          // Tab prepends.
          if (!remove)
          {
            prepend = use_spaces ? ' '.repeat(tab_sz) : '\t';
            lines[n] = prepend + lines[n];
            original_start += ((n == 0) ? prepend.length : 0);
            original_end   +=             prepend.length;
          }
          // SHIFT-Tab removes from start of line.
          else
          {
            // Single tabs.
            if (lines[n].startsWith('\t'))
            {
              lines[n] = lines[n].substr(1);
              anchor  = selection
                      ? selection_start
                      : Math.max(selection_start, first_line_start);
              original_start = Math.max((original_start - ((n == 0) ? 1 : 0)), anchor);
              original_end   = selection
                             ? original_end - 1
                             : original_start;
            }
            // Also removes run of spaces up to text-area's tab_sz, with or
            // without use_spaces.
            else
            {
              spcs = tab_sz;
              while (spcs > 0)
              {
                if (lines[n].startsWith(' '.repeat(spcs)))
                {
                  lines[n] = lines[n].substr(spcs);
                  anchor  = selection
                          ? selection_start
                          : Math.max(selection_start, first_line_start);
                  original_start = Math.max((original_start - ((n == 0) ? spcs : 0)), anchor);
                  original_end   = selection
                                 ? original_end - spcs
                                 : original_start;
                  break;
                }
                spcs--;
              }
            }
          }
        }
        // Apply expanded whole-line selection points to textarea.
        target.selectionStart = selection_start;
        target.selectionEnd   = selection_end;
        // Insert replacement text.
        document.execCommand('insertText', false, lines.join('\n'));
      }
      // Scroll to make caret visible, and then restore original selection,
      // adjusted based on how many characters were inserted or removed.
      target.selectionStart = original_start;
      target.selectionEnd   = original_start;
      target.blur();
      target.focus();
      target.selectionEnd   = original_end;
      break;
    // Unhandled keys.
    default:
      break;
  }

  // Manages breaking away from Tab key capture using Esc.
  tab_editor_enabled = next_enabled_state;

}
textarea {
  tab-size: 4;
}
<textarea
  rows="16"
  cols="132"
  spellcheck="false"
  tabindex="1">This is a normal textarea input where tab is not handled.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua.  Ut enim ad minim veniam, quis
nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
fugiat nulla pariatur.  Excepteur sint occaecat cupidatat non proident, sunt in
culpa qui officia deserunt mollit anim id est laborum.  Lorem ipsum dolor sit
amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
et dolore magna aliqua.  Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat.  Duis aute irure dolor
in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia
deserunt mollit anim id est laborum.</textarea>

<br>
<br>

<textarea
  rows="16"
  cols="132"
  tabindex="2"
  spellcheck="false"
  onkeydown="tab_editor(this);">This is a textarea input using tab_editor.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua.  Ut enim ad minim veniam, quis
nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
fugiat nulla pariatur.  Excepteur sint occaecat cupidatat non proident, sunt in
culpa qui officia deserunt mollit anim id est laborum.  Lorem ipsum dolor sit
amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
et dolore magna aliqua.  Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat.  Duis aute irure dolor
in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia
deserunt mollit anim id est laborum.</textarea>

<br>
<br>

<textarea
  rows="16"
  cols="132"
  tabindex="3"
  spellcheck="false"
  style="tab-size: 8;"
  onkeydown="tab_editor(this, true);">This is a textarea input using tab_editor using spaces instead of tabs.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua.  Ut enim ad minim veniam, quis
nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
fugiat nulla pariatur.  Excepteur sint occaecat cupidatat non proident, sunt in
culpa qui officia deserunt mollit anim id est laborum.  Lorem ipsum dolor sit
amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
et dolore magna aliqua.  Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat.  Duis aute irure dolor
in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia
deserunt mollit anim id est laborum.</textarea>
joeymorin
  • 49
  • 2
0

The shortest oneliner:

<textarea onkeydown="if(event.keyCode===9){this.setRangeText('\t',this.selectionStart,this.selectionEnd,'end');return false;}">
</textarea>
hlorand
  • 1,070
  • 10
  • 8
-1
if (e.which == 9) {
    e.preventDefault();
    var start = $(this).get(0).selectionStart;
    var end = $(this).get(0).selectionEnd;

    if (start === end) {
        $(this).val($(this).val().substring(0, start)
                    + "\t"
                    + $(this).val().substring(end));
        $(this).get(0).selectionStart =
        $(this).get(0).selectionEnd = start + 1;
    } else {
        var sel = $(this).val().substring(start, end),
            find = /\n/g,
            count = sel.match(find) ? sel.match(find).length : 0;
        $(this).val($(this).val().substring(0, start)
                    + "\t"
                    + sel.replace(find, "\n\t")
                    + $(this).val().substring(end, $(this).val().length));
        $(this).get(0).selectionStart =
        $(this).get(0).selectionEnd = end+count+1;
    }
}
razorxan
  • 516
  • 1
  • 7
  • 20
-1

Try this simple jQuery function:

$.fn.getTab = function () {
    this.keydown(function (e) {
        if (e.keyCode === 9) {
            var val = this.value,
                start = this.selectionStart,
                end = this.selectionEnd;
            this.value = val.substring(0, start) + '\t' + val.substring(end);
            this.selectionStart = this.selectionEnd = start + 1;
            return false;
        }
        return true;
    });
    return this;
};

$("textarea").getTab();
// You can also use $("input").getTab();
-1

I had to make a function to do the same, It is simple to use, just copy this code to your script and use: enableTab( HTMLElement ) HTMLelement being something like document.getElementById( id )


The code is:
function enableTab(t){t.onkeydown=function(t){if(9===t.keyCode){var e=this.value,n=this.selectionStart,i=this.selectionEnd;return this.value=e.substring(0,n)+" "+e.substring(i),this.selectionStart=this.selectionEnd=n+1,!1}}}
Dendromaniac
  • 378
  • 1
  • 14
-1
$("textarea").keydown(function(event) {
    if(event.which===9){
        var cIndex=this.selectionStart;
        this.value=[this.value.slice(0,cIndex),//Slice at cursor index
            "\t",                              //Add Tab
            this.value.slice(cIndex)].join('');//Join with the end
        event.stopPropagation();
        event.preventDefault();                //Don't quit the area
        this.selectionStart=cIndex+1;
        this.selectionEnd=cIndex+1;            //Keep the cursor in the right index
    }
});
-3

If you really need tabs copy a tab from word or notepad and paste it in the text box where you want it

1 2 3

12 22 33

Unfortunately I think they remove the tabs from these comments though :) It will show as %09 in your POST or GET