使用制表符在文本区缩进

我在我的网站上有一个简单的 HTML 文本区。

现在,如果您在其中单击 标签,它将转到下一个字段。我想使选项卡按钮缩进几个空格代替。

我怎么能这么做?

148962 次浏览

您必须编写 JS 代码来捕捉按 TAB 键并插入一些空格。类似于 JSFiddle 的功能。

检查 jquery 小提琴:

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');
}
});
​

大量借鉴其他类似问题的答案(张贴在下面) ..。

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: 如何捕获 Textbox 中的 TAB 按键

如何在 textarea 中处理 < tab > ?

正如其他人所写的那样,您可以使用 JavaScript 来捕获事件、防止默认操作(这样光标就不会转移焦点)并插入制表符。

但是 ,禁用默认行为使得不使用鼠标就不可能将焦点移出文本区域。盲人用户只能使用键盘和网页进行交互——他们看不到鼠标指针做任何有用的事情,所以要么使用键盘,要么什么都不用。选项卡键是导航文档(尤其是窗体)的主要方式。重写选项卡键的默认行为将使盲用户无法将焦点移动到下一个表单元素。

所以,如果你正在为一个广泛的读者写一个网站,我建议不要在没有 引人注目原因的情况下这样做,并为盲人用户提供一些替代方案,不要把他们困在文本区域。

按住 ALT 键,从数字键盘上按0,9。它可以在 google-chrome 中工作

基于人们在这里所说的答案,它只是一个组合键入(而不是键入) + 防止默认() + 插入一个标签字符在插入符号。比如:

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

早期的答案有一个可以工作的 jsfiddle,但是它在 keydown 上使用了 alert ()。如果您删除这个警报,那么它不工作。我刚刚添加了一个函数,在文本区的当前光标位置插入一个选项卡。

下面是一个相同的工作 jsfiddle: Http://jsfiddle.net/nshgz/

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;
}
}
}

这个解决方案不需要 jQuery,并将在页面上的所有文本区域上启用选项卡功能。

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;
}
}

试试这个简单的 jQuery 函数:

$.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();

基于@kasdega 解决方案的多行折叠脚本。

$('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;
}
});

在 AngularJS 环境下使用@kasdega 的回答,我毫无进展,似乎没有任何尝试能够让 Angularact 对这种变化作出反应。因此,为了防止它对路人有任何用处,这里重写了@kasdega 的代码,AngularJS 风格,对我很有用:

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');
}
});
};
});

以及:

<textarea ng-model="mytext" ng-allow-tab></textarea>

这个解决方案允许像典型的代码编辑器一样在整个选项中使用选项卡,并且也可以取消选项卡。然而,我还没有弄明白如何在没有选择的情况下实现 shift-tab。

$('#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>

如果你真的需要选项卡,从单词或记事本上复制一个选项卡,粘贴到你想要的文本框中

123

122233

不幸的是,我认为他们从这些评论中删除了标签:) 它将在您的 POST 或 GET 中显示为% 09

我不得不创建一个函数来做同样的事情,这是很简单的使用,只要把这个代码复制到你的脚本中,然后使用: enableTab( HTMLElement ) HTMLelement 类似于 document.getElementById( id )


密码是:

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}}}

以上答案全部擦除撤消历史。对于那些想找到一个不这样做的解决方案的人,我花了一个小时为 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);
});
};

简而言之,选项卡插入到所选行的开头。

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

要点: https://gist.github.com/iautomation/e53647be326cb7d7112d

示例用法: $('textarea').enableTabs('\t')

缺点: 只能在 Chrome 上运行。

Github 上有一个库,wjbryant: 标签覆盖可以在你的文本区域提供选项卡支持

事情是这样的:

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


// enable Tab Override for all textareas
tabOverride.set(textareas);

不管怎样,这是我的一段话,为了你们在这个帖子里一直在讨论的东西:

<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>

测试最新版本的 Chrome,Firefox,Internet Explorer 和 Edge。

这是我的版本,支持:

  • 标签 + 移位标签
  • 维护简单制表符插入的撤消堆栈
  • 支持块行缩进/取消缩进,但垃圾回收堆栈
  • 在块缩进/取消缩进时正确选择整行
  • 支持在按下输入键时自动缩进(维护撤消堆栈)
  • 使用 Escape 键取消对下一个选项卡/输入键的支持(这样您可以按 Escape 然后选项卡出)
  • 可以在 Chrome + Edge 上工作,其他未经测试的也可以。

$(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>

我做了一个,你可以访问任何文本元素,你喜欢:

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;
}
}

元素看起来像这样:

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

每个文本元素的输入都有一个 onkeydown 事件。在事件处理程序中,只要 event.keyCode 为9,就可以使用 Event.preventDefault () 防止 tab 键的默认反应。

然后把标签放在正确的位置:

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>

我看到这个问题还没有解决,我编写了这个程序,它运行得很好。 它在光标索引处插入一个表格,而不使用 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>
$("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
}
});

现代化的方式,既直接又不失 解除的能力(Ctrl + Z)的最后变化。

$('#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));
}
});

更多关于 execCommand:


编辑:

正如评论中指出的那样(虽然这个 曾经是一个“现代”的解决方案) ,这个特性已经过时了:

这个特性已经过时了。虽然它可能仍然在某些浏览器中工作,但是它的使用是不鼓励的,因为它可能随时被删除。尽量避免使用它。

简单的独立脚本:

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;
}
}
}

我发现 最简单的方法普通的 JavaScript一起做的 在现代浏览器中是:

  <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>

注意,为了简单起见,我在这个代码片段中使用了很多 ES6特性,您可能希望在部署它之前对其进行翻译(使用 Babel 或 TypeScript)。

作为上面 kasdega 代码的一个选项,您可以在当前光标点插入字符,而不是将选项卡附加到当前值。这样做的好处是:

  • 允许您插入4个空格作为制表符的替代
  • 撤消和重做将与插入的字符一起工作(它不会与 OP)

那就换掉

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

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

您可以使用 textarea元素上可用的 setRangeText()方法本地执行此操作。

超文本标示语言

<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用于替换文本,但是因为我们只想插入一个 \t,所以我们只需将所选内容设置为当前所选内容的开始和结束。'end'值告诉该方法将光标移动到插入文本的末尾。

额外的 CSS

如果要更改制表符大小,可以对块元素使用 tab-size属性。大多数浏览器的默认值是 8

textarea {
tab-size: 4;
}

Mozilla: HTMLInputElement.setRangeText ()

Mozzila: 标签大小

下面是一个简单的纯 JS 方法,它支持基本的缩进和缩进。

不幸的是,它不能保留撤消历史记录或支持块级别选项卡。

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;
}
}
}
});

我尝试了一些解决方案,但没有一个奏效,所以我想到了这个:

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));
}
});

我没有足够好的声誉来发表评论,否则我会把这个作为评论添加到 Brad Robinson 的回答,作为 gcoulby 评论的后续。

我受到了两者的启发,结果就像小提琴一样: Https://jsfiddle.net/qmyh76tu/1/

... 但也作为这个答案的一个片段,因为我不能发布一个小提琴没有发布代码。

这个版本做了一些额外的事情,最值得注意的是:

  • 完全保留撤消堆栈
  • 在纯 JS 中实现

编辑2022-10-29: 对代码片段和小提琴进行小改动,以修复我在试图用光标在行首打印时发现的问题。还添加了一个额外的文本区域,以便在不使用 tab _ Editor ()的情况下进行比较。

编辑2022-10-30: 修复自动缩进问题和添加回程/结束弹跳的另一个小变化。

// 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>