71

I would like a textarea that handles a situation of pressing tab key.

In default case if you press a tab key then focus leaves the textarea. But what about the situation when user wants to type tab key in textarea?

Can I catch this event and return focus to the textarea and add a tab to a current cursor position?

sergzach
  • 6,578
  • 7
  • 46
  • 84
  • Possible duplicate of [Use tab to indent in textarea](http://stackoverflow.com/questions/6637341/use-tab-to-indent-in-textarea) – Pere Jan 02 '17 at 16:18
  • I reviewed some of the existing answers and they all fall short in some way (no undo, poor performance, incorrect selection after unindent). I recently wrote a small module that handles this correctly, called [`indent-textarea`](https://github.com/fregante/indent-textarea) – fregante Dec 15 '19 at 21:07

6 Answers6

131

You can: http://jsfiddle.net/sdDVf/8/.


$("textarea").keydown(function(e) {
    if(e.keyCode === 9) { // tab was pressed
        // get caret position/selection
        var start = this.selectionStart;
        var end = this.selectionEnd;

        var $this = $(this);
        var value = $this.val();

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

        // put caret at right position again (add one for the tab)
        this.selectionStart = this.selectionEnd = start + 1;

        // prevent the focus lose
        e.preventDefault();
    }
});
pimvdb
  • 151,816
  • 78
  • 307
  • 352
  • 2
    @Amine: To prevent the default tab function of the browser. I now see it's not necessary. I'm going to have a search on this. EDIT: `return false` seems to include `preventDefault`: http://stackoverflow.com/questions/1357118/javascript-event-preventdefault-vs-return-false. – pimvdb May 26 '11 at 15:01
  • 1
    Implemented selection overwrite too. – pimvdb May 27 '11 at 16:12
  • @sergzach: My solution does not work at all in IE8, because it does not support `selectionStart`/`selectionEnd`. You'd have to create text ranges, but I still don't understand how that works I'm afraid... – pimvdb Jul 05 '11 at 20:49
  • Is there any way at all to _make_ it work in IE8? It's a real shame it doesn't :C – Bojangles Jul 16 '11 at 17:30
  • @JamWaffles: This solution by Tim Down is said to work cross-browser: http://stackoverflow.com/questions/263743/how-to-get-cursor-position-in-textarea/3373056#3373056. It is my firm conviction that it can be combined with my answer somehow. – pimvdb Jul 16 '11 at 20:45
  • Thanks very much pimvdb - very handy. – Bojangles Jul 16 '11 at 20:56
  • Obviously there is a solution, as how does JSFiddle do it? – Lucas Aug 28 '12 at 07:49
  • @think123: Do you mean how jsFiddle's editor handles the tab key? I'm not sure but of course you could dig into its source :) – pimvdb Aug 28 '12 at 09:19
  • Perhaps a bit tardy to the party, but for all those interested, there's been a [short library that manages this for you](https://github.com/julianlam/tabIndent.js/). – Julian H. Lam May 12 '13 at 13:53
  • I'd like to do group indentation as well via selection+tab. How would this be achieved? – Michael Schwartz Aug 30 '13 at 04:31
  • This works if I place the character infront of a line, but if I select a few lines, it replaces the selection with a tab instead of indenting the selection. – chovy Sep 10 '13 at 06:18
  • 2
    This brakes the browsers undo feature (Ctrl+z) – 01AutoMonkey Mar 23 '17 at 19:49
  • 1
    Not sure how/why this was missed, on the last substring call _start_ should be given as the first argument: `+ value.substring(start, end));` -- Otherwise tabbing when selecting text erases the text. – Asaf May 22 '17 at 15:10
30

Here is a modified version of pimvdb's answer that doesn't need JQuery:

document.querySelector("textarea").addEventListener('keydown',function(e) {
    if(e.keyCode === 9) { // tab was pressed
        // get caret position/selection
        var start = this.selectionStart;
        var end = this.selectionEnd;

        var target = e.target;
        var value = target.value;

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

        // put caret at right position again (add one for the tab)
        this.selectionStart = this.selectionEnd = start + 1;

        // prevent the focus lose
        e.preventDefault();
    }
},false);

I tested it in Firefox 21.0 and Chrome 27. Don't know if it works anywhere else.

alexwells
  • 1,243
  • 13
  • 16
  • 1
    To apply this to all textarea use querySelectorAll, enumerate the list returned, and add the event listener to each element. – Jeffrey LeCours Sep 17 '14 at 20:02
  • To get this approach to work, you need to attach the __eventListener__ to **individual** DOM elements. To do that: 1) get all the elements `document.querySelectorAll("textarea")` 2) loop though the array to attach the event listener to each element. Then it should work – Amjad Feb 20 '18 at 11:48
  • Breaks the undo functionality as the accepted answer. – Daniel May 26 '18 at 18:49
14

Good god, all previous answers failed to provide the commonly decent (i.e. for programmers) tab control.

That is, a hitting TAB on selection of lines will indent those lines, and SHIFTTAB will un-indent them.

_edited (Nov 2016): keyCode replaced with charCode || keyCode, per KeyboardEvent.charCode - Web APIs | MDN

(function($) {
  $.fn.enableSmartTab = function() {
    var $this;
    $this = $(this);
    $this.keydown(function(e) {
      var after, before, end, lastNewLine, changeLength, re, replace, selection, start, val;
      if ((e.charCode === 9 || e.keyCode === 9) && !e.altKey && !e.ctrlKey && !e.metaKey) {
        e.preventDefault();
        start = this.selectionStart;
        end = this.selectionEnd;
        val = $this.val();
        before = val.substring(0, start);
        after = val.substring(end);
        replace = true;
        if (start !== end) {
          selection = val.substring(start, end);
          if (~selection.indexOf('\n')) {
            replace = false;
            changeLength = 0;
            lastNewLine = before.lastIndexOf('\n');
            if (!~lastNewLine) {
              selection = before + selection;
              changeLength = before.length;
              before = '';
            } else {
              selection = before.substring(lastNewLine) + selection;
              changeLength = before.length - lastNewLine;
              before = before.substring(0, lastNewLine);
            }
            if (e.shiftKey) {
              re = /(\n|^)(\t|[ ]{1,8})/g;
              if (selection.match(re)) {
                start--;
                changeLength--;
              }
              selection = selection.replace(re, '$1');
            } else {
              selection = selection.replace(/(\n|^)/g, '$1\t');
              start++;
              changeLength++;
            }
            $this.val(before + selection + after);
            this.selectionStart = start;
            this.selectionEnd = start + selection.length - changeLength;
          }
        }
        if (replace && !e.shiftKey) {
          $this.val(before + '\t' + after);
          this.selectionStart = this.selectionEnd = start + 1;
        }
      }
    });
  };
})(jQuery);

$(function() {
  $("textarea").enableSmartTab();
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<textarea rows="10" cols="80">
/* Just some code to edit with our new superTab */
(function($) {
    $.fn.enableSmartTab = function() {
        $this = $(this);
        $this.keydown(function(e) {
            if ((e.charCode === 9 || e.keyCode === 9) && !e.metaKey && !e.ctrlKey && !e.altKey) {
                e.preventDefault();
            }
        }
    }
}
</textarea>
Orwellophile
  • 13,235
  • 3
  • 69
  • 45
  • 1
    I've just write [my code](https://github.com/wlzla000/enableTabIndentation/blob/master/enableTabIndentation.js) to achieve this feature. **In circumstances where you must not use jQuery**, consider using my code. – Константин Ван Nov 19 '16 at 09:18
  • 1
    @K._ nice code, but I would recommend running it through babel or google closure to remove the ES6 components. ES6 is great, but there are just too many users who aren't using ES6 compatible browsers. Also `event.key` is not (IIRC) consistent across browsers, keys maybe called different things in different browsers, and Safari doesn't support the property at all. This is the **other** reason people use jQuery, although often the forgotten one, it's to guarantee compatibility across browsers. I'm not criticizing your efforts, I think you did great work, but it has to work be cross-browser – Orwellophile Nov 20 '16 at 02:58
  • @K._ if you look at the code I wrote, there's actually not very much jQuery in there. Excluding the listener addition, the only actual use of jQuery is `$.fn.val`. The handler itself already uses native event properties already. – Orwellophile Nov 20 '16 at 03:15
  • 1
    What does ~ and !~ mean? – 1.21 gigawatts Feb 23 '19 at 04:07
  • 1
    @1.21gigawatts it's the bitwise not operator, there's a pretty good explanation for why it is used here [here](https://wsvincent.com/javascript-tilde/) – peabrainiac Oct 17 '19 at 23:20
6

In Vanilla (Default) JS this would be:

var textareas = document.getElementsByTagName('textarea');

if ( textareas ) {
  for ( var i = 0; i < textareas.length; i++ ) {
textareas[i].addEventListener( 'keydown', function ( e ) {
  if ( e.which != 9 ) return;

  var start             = this.selectionStart;
  var end                 = this.selectionEnd;

  this.value            = this.value.substr( 0, start ) + "\t" + this.value.substr( end );
  this.selectionStart = this.selectionEnd = start + 1;

  e.preventDefault();
  return false;
});
  }
}
textarea {
   border: 1px solid #cfcfcf;
   width: 100%;
   margin-left: 0px;
   top: 0px;
   bottom: 0px;
   position: absolute;
}
<textarea>
var x = 10;
var y = 10;
</textarea>
Mark
  • 16,906
  • 20
  • 84
  • 117
1

Found this while searching google. I made a really short one that can also indent and reverse indent selections of text:

    jQ(document).on('keydown', 'textarea', function(e) {
        if (e.keyCode !== 9) return;
        var Z;
        var S = this.selectionStart;
        var E = Z = this.selectionEnd;
        var A = this.value.slice(S, E);
        A = A.split('\n');
        if (!e.shiftKey)
            for (var x in A) {
                A[x] = '\t' + A[x];
                Z++;
            }
        else
            for (var x in A) {
                if (A[x][0] == '\t')
                    A[x] = A[x].substr(1);
                Z--;
            }
        A = A.join('\n');
        this.value = this.value.slice(0, S) + A + this.value.slice(E);
        this.selectionStart = S != E ? S : Z;;
        this.selectionEnd = Z;
        e.preventDefault();
    });
John Smith
  • 570
  • 2
  • 7
  • 17
1

Enable tabbing inside (multiple) textarea elements

Correcting @alexwells answer and enable a live demo

var textAreaArray = document.querySelectorAll("textarea");
    for (var i = textAreaArray.length-1; i >=0;i--){
        textAreaArray[i].addEventListener('keydown',function(e) {
            if(e.keyCode === 9) { // tab was pressed
                // get caret position/selection
                var start = this.selectionStart;
                var end = this.selectionEnd;

                var target = e.target;
                var value = target.value;

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

                // put caret at right position again (add one for the tab)
                this.selectionStart = this.selectionEnd = start + 1;

                // prevent the focus lose
                e.preventDefault();
            }
        },false);
    }
<textarea rows="10" cols="80"></textarea>
   <textarea rows="10" cols="80"></textarea>
Amjad
  • 3,110
  • 2
  • 20
  • 19