如何在HTML文本框中添加自动缩进?

5
我有一个HTML文本框。我想修改它,使其实现自动缩进,即在插入换行符后,我希望自动在新行开头插入空格(空格数量取决于上一行的缩进)。我发现,我可以通过注册监听“keypress”事件的处理程序来实现它。现在我有一个选择:(a)保留默认处理程序并在浏览器将换行符添加到textarea.value之后插入空格,或者(b)使用preventDefault()并自己插入整个内容(即换行符和空格)。
在情况(a)下,如下面的代码所示,我的处理程序在浏览器添加换行符之前执行,因此空格(或'--'用于说明)出现在行的末尾,而不是新行的开头。
在情况(b)下,如下面的代码中的注释所示,文本被正确修改,但是如果结果导致光标超出textarea视图,则内容不会滚动(很可能是因为内容滚动是默认处理的一部分),因此光标消失在textarea边界后面,只有当我发送另一个按键时(即不是换行符)才会重新出现。
如何在不失去默认滚动的情况下实现自动缩进效果?
我知道可以通过延迟插入空格(例如使用setTimeout())来近似实现此效果,以便运行时有足够的时间完成默认处理(即插入换行符和垂直滚动),但这对我来说似乎是一个巨大的修补程序,并且引入了一种我担心会在最不希望的情况下影响我的竞争条件(大规模复制粘贴,由于其他操作而导致运行时变慢,高键盘重复率等)。理想情况下,我希望(i)在默认处理之后调用我的代码或(ii)能够防止默认处理,运行我的代码,并明确调用默认处理。如何实现?
谢谢!
Greg
PS:我不感兴趣集成复杂的文本框替代品,例如Editarea(我使用一个,它在各个浏览器中非常脆弱)。
在FF3上测试过。
<html>
  <head>
    <script type="text/javascript">
      function onKeyPressHandler(e) {
      if (e.which == 13) // ASCII newline
          {
              var start = this.selectionStart;
              var end = this.selectionEnd;
              var v = this.value;
              this.value = v.slice(0, start) + '--' + v.slice(end); // (a)

              // (b): this.value = v.slice(0, start) + '\n--' + v.slice(end);
              // (b): e.preventDefault();
      }
      }

      onload = function() {
      var editor = document.getElementById("editor");
      editor.addEventListener('keypress', onKeyPressHandler, false);
      } 
    </script>
  </head>
  <body>
    <textarea rows="20" cols="80" id="editor"></textarea>
  </body>
</html>
7个回答

4
我修改了Leo的答案以解决延迟问题(通过使用keypress而不是keyup和setTimeout),并修复了导致无法编辑文本中间的错误。

$("textarea").keydown(function(e)
{
    if (e.which == 9) //ASCII tab
    {
        e.preventDefault();
        var start = this.selectionStart;
        var end = this.selectionEnd;
        var v = $(this).val();
        if (start == end)
        {
            $(this).val(v.slice(0, start) + "    " + v.slice(start));
            this.selectionStart = start+4;
            this.selectionEnd = start+4;
            return;
        }

        var selectedLines = [];
        var inSelection = false;
        var lineNumber = 0;
        for (var i = 0; i < v.length; i++)
        {
            if (i == start)
            {
                inSelection = true;
                selectedLines.push(lineNumber);
            }
            if (i >= end)
                inSelection = false;

            if (v[i] == "\n")
            {
                lineNumber++;
                if (inSelection)
                    selectedLines.push(lineNumber);
            }
        }
        var lines = v.split("\n");
        for (var i = 0; i < selectedLines.length; i++)
        {
            lines[selectedLines[i]] = "    " + lines[selectedLines[i]];
        }

        $(this).val(lines.join("\n"));
    }
});
$("textarea").keypress(function(e)
{
    if (e.which == 13) // ASCII newline
    {
        setTimeout(function(that)
        {
            var start = that.selectionStart;
            var v = $(that).val();
            var thisLine = "";
            var indentation = 0;
            for (var i = start-2; i >= 0 && v[i] != "\n"; i--)
            {
                thisLine = v[i] + thisLine;
            }
            for (var i = 0; i < thisLine.length && thisLine[i] == " "; i++)
            {

                indentation++;
             }
             $(that).val(v.slice(0, start) + " ".repeat(indentation) + v.slice(start));
             that.selectionStart = start+indentation;
             that.selectionEnd = start+indentation;  
}, 0.01, this);
     }
});
<textarea rows="20" cols="40"></textarea>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>


0

这是很多借用的代码(感谢stackoverflow!),稍作修改。第一部分只是创建一个镜像,以便您知道自己在哪一行(与您的问题无关),但它包含了设置当前缩进的内容,这很重要。

$(document).keypress(function(e) {
  if (e.keyCode ==13){
    e.preventDefault();
    var start = $('textarea').get(0).selectionStart;
    var end = $('textarea').get(0).selectionEnd;
    // set textarea value to: text before caret + tab + text after caret
    var spaces = "\n" 
    for (i = 0; i < start; i++) { 
      spaces += " "
    }
   $('textarea').val($('textarea').val().substring(0, start)
      + spaces 
      + $('textarea').val().substring(end));

    // put caret at right position again
    console.log(spaces.length)
    $('textarea').get(0).selectionStart =
    $('textarea').get(0).selectionEnd = start + spaces.length;
  }
})

除非我漏掉了什么,否则这个答案中的逻辑是有问题的。如果你在第一行输入类似于<html>的内容并按回车键,你的代码会添加一个新行和六个空格(因为start是6)。然后输入<body>并按回车键。这将添加一个新行和16个空格。它需要找到前一行代码开始之前的空格数量,然后添加相应数量的空格。 - John Washam

0

谢谢。不幸的是,http://thelackthereof.org/JQuery_Autoindent 正好有我所描述的问题——当光标因为一系列NEWLINE键事件而超出可见区域时,没有垂直滚动(至少在FF3中)。据我所知,这是由于该解决方案中默认处理程序被抑制(使用e.preventDefault())所致。 - Grzegorz Jakacki
我不明白你在说什么,http://plugins.jquery.com/content/optional-auto-indent-setting,据我所知这只是一个功能请求,而不是解决方案。 - Grzegorz Jakacki

0

虽然这篇文章已经写了将近六年,但是以下是如何自动缩进textarea的方法:

$("textarea").keydown(function(e)
{
    if (e.which == 9) //ASCII tab
    {
        e.preventDefault();
        var start = this.selectionStart;
        var end = this.selectionEnd;
        var v = $(this).val();
        if (start == end)
        {
            $(this).val(v.slice(0, start) + "    " + v.slice(start));
            return;
        }

        var selectedLines = [];
        var inSelection = false;
        var lineNumber = 0;
        for (var i = 0; i < v.length; i++)
        {
            if (i == start)
            {
                inSelection = true;
                selectedLines.push(lineNumber);
            }
            if (i >= end)
                inSelection = false;

            if (v[i] == "\n")
            {
                lineNumber++;
                if (inSelection)
                    selectedLines.push(lineNumber);
            }
        }
        var lines = v.split("\n");
        for (var i = 0; i < selectedLines.length; i++)
        {
            lines[selectedLines[i]] = "    " + lines[selectedLines[i]];
        }

        $(this).val(lines.join("\n"));
    }
});
$("textarea").keyup(function(e)
{
    if (e.which == 13) // ASCII newline
    {
        var start = this.selectionStart;
        var v = $(this).val();
        var thisLine = "";
        var indentation = 0;
        for (var i = start-2; i >= 0 && v[i] != "\n"; i--)
        {
            thisLine = v[i] + thisLine;
        }
        for (var i = 0; i < thisLine.length && thisLine[i] == " "; i++)
        {

            indentation++;
        }
        $(this).val(v.slice(0, start) + " ".repeat(indentation) + v.slice(start));
    }

});
<textarea rows="20" cols="40"></textarea>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>

不幸的是,由于它绑定到keyup,当您按住回车键时,光标将位于下一行的开头。只有在释放回车键时,它才会缩进新行。这意味着如果您轻敲回车键,它将延迟缩进:
自动缩进


你需要设置selectionStart和selectionEnd,否则光标会移动到末尾(至少在Chrome中是这样)。另外,如果你打字很快,有时候由于延迟缩进不会被执行。 - CpnCrunch

0

最近我也想做这个,但发现使用JavaScript更改textarea的值的缺点是会意外地破坏您的编辑历史(没有超过最后一个换行符条目的撤消)。不过,我通过使用以下代码成功实现了它:

/**
 * Implements auto-indenting in a textarea so that when you hit enter the same
 * indentation as used on the previous line will be used in the new line.  Also
 * makes it so that pressing tab will add a tab character where the cursor is.
 * 
 * WARNING:  This solution clobbers edit history (undo and redo).
 * 
 * @param {HTMLTextAreaElement} textarea
 *   The textarea to auto-indent.
 */
function autoIndent(textarea) {
  textarea.addEventListener('keydown', event => {
    const isEnter = event.which === 13;
    const isTab = event.which === 9;
    if (isEnter || isTab) {
      event.preventDefault();
      const {selectionStart, value} = textarea;
      const insertion = isEnter
        ? '\n' + (value.slice(0, selectionStart).match(/(?:^|[\r\n])((?:(?=[^\r\n])[\s])*?)\S[^\r\n]*\s*$/) || [0, ''])[1]
        : '\t';
      textarea.value = value.slice(0, selectionStart) + insertion + value.slice(selectionStart);
      textarea.selectionEnd = textarea.selectionStart = selectionStart + insertion.length;
      // Attempts to scroll to the next line but will not work if indentation is extreme.
      textarea.scrollTop += textarea.clientHeight / textarea.rows;
    }
  });
}

window.addEventListener('DOMContentLoaded', () => {
  autoIndent(document.querySelector('textarea'));
});
<textarea style="width: 100%; box-sizing: border-box;" rows="10">
window.addEventListener('DOMContentLoaded', () => {
  // This is an example
  autoIndent(document.querySelector('textarea'));
});
</textarea>

目前我的Gist在这里,但我希望继续努力解决编辑历史问题。


0

寻找了一圈,我从未找到适用于所有情况的良好缩进/取消缩进的方法。下面所示的方法支持以下功能,并且不依赖于jQuery。

功能:

  • 可定制代码的缩进级别
  • 按下 Enter 键时,在新行上保持缩进级别
  • 在任何位置按下 Tab 键,将在当前行的开头缩进
  • 在任何位置按下 Shift + Tab 键,将取消缩进当前行

注意:这需要在 keydown 事件中进行,因为这将为此代码提供修改之前的值

const editorIndentSpaces = 2;
const indent = " ".repeat(editorIndentSpaces);
const unIndentPattern = new RegExp(`^ {${editorIndentSpaces}}`);

document.querySelector("textarea")
  .addEventListener("keydown", ev => {
    const textarea = ev.target;
    const v = textarea.value;
    const startPos = textarea.selectionStart;
    const endPos = textarea.selectionEnd;
    if (ev.key === "Tab") {
      ev.preventDefault(); //stop the focus from changing
      const isUnIndenting = ev.shiftKey;

      if (startPos === endPos) {
        //nothing selected, just indent/unindent where the cursor is
        let newCursorPos;
        const lineStartPos = v.slice(0, startPos).lastIndexOf("\n") + 1;
        const lineEndPos = v.slice(lineStartPos, v.length).indexOf("/n");
        if (isUnIndenting) {
          const newLineContent = v
            .slice(lineStartPos, lineEndPos)
            .replace(unIndentPattern, "");
          textarea.value =
            v.slice(0, lineStartPos) + newLineContent + v.slice(lineEndPos);
          newCursorPos = Math.max(startPos - editorIndentSpaces, lineStartPos);
        } else {
          textarea.value =
            v.slice(0, lineStartPos) + indent + v.slice(lineStartPos);
          newCursorPos = startPos + editorIndentSpaces;
        }
        textarea.setSelectionRange(newCursorPos, newCursorPos);
      } else {
        //Indent/unindent the selected text
        const lineStartPos = v.slice(0, startPos).lastIndexOf("\n") + 1;
        const selection = v.substring(lineStartPos, endPos);
        let result = "";
        const lines = selection.split("\n");
        for (let i = 0; i < lines.length; i++) {
          if (isUnIndenting) {
            //unindent selected lines
            result += lines[i].replace(unIndentPattern, "");
          } else {
            //Indent selected lines
            result += indent + lines[i];
          }

          if (i < lines.length - 1) {
            //add line breaks after all but the last line
            result += "\n";
          }
        }

        textarea.value = v.split(selection).join(result);
        if (isUnIndenting) {
          textarea.setSelectionRange(
            Math.max(startPos - editorIndentSpaces, lineStartPos),
            lineStartPos + result.length
          );
        } else {
          textarea.setSelectionRange(
            startPos + editorIndentSpaces,
            lineStartPos + result.length
          );
        }
      }
    } else if (ev.key === "Enter") {
      //When enter is pressed, maintain the current indentation level

      //We will place the newline character manually, this stops it from being typed
      ev.preventDefault();

      //Get the current indentation level and prefix the new line with the same
      const prevLinePos = v.slice(0, startPos).lastIndexOf("\n") + 1;
      const prevLine = v.slice(prevLinePos, endPos);
      const levels = prevLine.match(/^ */)[0].length / editorIndentSpaces;
      const indentation = indent.repeat(levels);
      textarea.value =
        v.slice(0, endPos) + "\n" + indentation + v.slice(endPos);

      //Set the cursor position
      const newCursorPos = endPos + 1 + indentation.length;
      textarea.setSelectionRange(newCursorPos, newCursorPos);
    }
  });
<textarea rows="10">sample test
line2
  line3
  line4</textarea>


0
除了其他人的答案,我也想分享一下我的尝试,即根据光标位置前一行的缩进来自动缩进textarea
以下是我的方法:
1.) 检测回车事件(就像你和其他人在这里做的那样) 2.) 获取相对于光标位置的前一行内容 3.) 获取缩进的大小 4.) 强制在光标位置插入空格/制表符
先看一下代码,然后在最后提供解释。
JavaScript代码:
textarea.addEventListener("keyup", function(event) {
    if (event.key == "Enter") {
        // enter key is pressed in textarea
        
        // get previous line content relative to cursor position
        var line = this.value.substring(0, this.selectionStart).split("\n");
        line = line[line.length - 2];  // string
        
        // getting the indentation
        var content_to_remove = line.trimStart();  // string
        var indentation = line.replace(content_to_remove, "");
        
        // insert indentation
        this.setRangeText(indentation, this.selectionStart, this.selectionEnd, "end");
    }
});

解释

var line = this.value.substring(0, this.selectionStart).split("\n");
从开头到光标位置提取文本,并按\n分割成列表。

line = line[line.length - 2]; // 字符串
获取列表的“倒数第二个元素”,即相对于当前光标位置的上一行。我不确定为什么要使用line.length - 2而不是line.length - 1

var content_to_remove = line.trimStart(); // 字符串
获取要删除的内容(即从开头删除所有空格,剩余部分即为要删除的内容)。通过获取这个内容,我们可以使用JavaScript字符串方法将其替换为"",从而帮助我们获取行的缩进:

var indentation = line.replace(content_to_remove, "");

this.setRangeText(indentation, this.selectionStart, this.selectionEnd, "end");
然后我们使用setRangeText方法将前一行提取的缩进插入到当前行。我对这个方法不太熟悉,所以请参考this answer


只是以防你想看一下结果(我不知道如何仅在代码片段中显示JS部分):

  var textarea = document.getElementById("inputTextarea");
  
  // auto indent when an Enter key is pressed
  // size of indentation depends on the previous line
  /* Procedure :
      1) detect on enter event
      2) get previous line content relative to cursor position
      3) get the size of indentation
      4) force insert spaces / tabs at cursor position
  */
  textarea.addEventListener("keyup", function(event) {
    if (event.key == "Enter") {
      // enter key is pressed in textarea

      // get previous line content relative to cursor position
      var line = this.value.substring(0, this.selectionStart).split("\n");
      line = line[line.length - 2]; // string

      // getting the indentation
      var content_to_remove = line.trimStart(); // string
      var indentation = line.replace(content_to_remove, "");

      // insert indentation
      this.setRangeText(indentation, this.selectionStart, this.selectionEnd, "end");
    }
  });
textarea {
    font-size: 13px;
    /* https://dev59.com/-3VD5IYBdhLWcg3wOo5h */
    /* word-wrap: break-word;
        word-break: break-all;
        text-wrap: unrestricted; */
    /* https://dev59.com/MHRC5IYBdhLWcg3wD8xV */
    white-space: nowrap;
    /* normal | nowrap */
    margin-left: 15px;
}
<!DOCTYPE html>

<html>

<head>
  <title>Test</title>
  <meta charset="UTF-8" name="viewport" content="width=device-width, initial-scale=1.0">
</head>


<body>
  <textarea id="inputTextarea" rows="10" cols="50" placeholder="I Will auto-indent your code :)" autofocus>Hi
    This line got an indent
        Another indented line</textarea>
</body>


</html>


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接