How to add autoindent to HTML textarea?
Asked Answered
Z

7

5

I have HTML textarea. I want to modify it, so that it implements auto-indentation, i.e. after NEWLINE is inserted, I want to automatically insert spaces at the beginning of the new line (the number of spaces depending on the indentation of the previous line). I figured out, that I can do it by registering a handler listening to 'keypress' event. Now I have a choice: (a) leave the default handler and insert the spaces AFTER the browser adds newline to the textarea.value, or (b) use preventDefault() and insert the whole thing (i.e. newline and spaces) by myself.

In case (a), illustrated by the code below, my handler is executed BEFORE the browser adds a newline, so the spaces (or '--' for illustration) end up at the end of the line, not at the beginning of the new one.

In case (b), shown in comments in the code below, the text is modified correctly, but if it results in cursor going out of the textarea view, the content is not scrolled (most likely because content scrolling is a part of default handling), so the cursor disappears behind the textarea boundary, and reappears only if I send another keystroke (i.e. not a newline).

How to achieve the auto-indentation effect without losing the default scrolling?

I know that this effect can possibly be approximated by delaying the insertion of spaces (e.g. with setTimeout()), so that the runtime has enough time to complete the default handling (i.e. insertion of the newline and vertical scrolling), but it seems like a huge kludge to me and introduction of a race condition that I am afraid is going to hit me under least expected circumstances (massive copy-paste, runtime slow due to other actions, high keyboard repetition rate etc.). Ideally I would like either (i) to have my code called after the default handling or (ii) to be able to prevent default handling, run my code, and explicitly call default handling. How to achieve it?

Thanks!

Greg

PS: I am not interested in integrating sophisticated textarea replacements, e.g. Editarea (I use one and it is very fragile across browsers).

Tested on 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>
Zucker answered 21/4, 2011 at 12:13 Comment(0)
L
5

I've modified Leo's answer to fix the delay problem (by using keypress instead of keyup with a setTimeout), and the bug that caused editing the middle of the text to not work.

$("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>
Lialiabilities answered 5/9, 2017 at 21:50 Comment(0)
U
3

Looking around I never found anything that did indenting/unindenting well for all cases. What I have below supports the following, and is not dependent on jQuery either.

Features:

  • customizable indentation level in code
  • When pressing Enter it maintains the indentation level on the new line
  • When pressing Tab anywhere it will indent at the beginning of the current line
  • When pressing Shift + Tab anywhere it will un-indent the current line

NOTE: This needs to occur on a keydown event since that will provide this code with the value before it has been changed

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>
Unionism answered 2/8, 2023 at 15:43 Comment(2)
This is truly great, just plugged this code into a textarea and went from barely being able to write code to getting stuff done just like that.Kubetz
One downside with my code is that any highlighted code is simply found by text match, so if you have two lines with identical text, highlight one and press tab, both lines will be indentedUnionism
L
1

I recently wanted to do this as well but found that the downside is that if you use JavaScript to change the value of textarea you will end up inadvertently clobbering your edit history (no more undos past the last line break entry). Still, I did get it to work by using the following code:

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

For now my Gist is here but I hope to continue working on it to fix the edit history issue.

Lot answered 30/6, 2022 at 16:58 Comment(0)
S
0

Check this out : http://thelackthereof.org/JQuery_Autoindent

and also, http://plugins.jquery.com/content/optional-auto-indent-setting

Solemn answered 21/4, 2011 at 12:19 Comment(2)
Thanks. Unfortunately thelackthereof.org/JQuery_Autoindent has exactly the problem I am describing -- no vertical scrolling when cursor makes it out of the visible area due to a sequence of NEWLINE key events (at least in FF3). AFAICT this is due to the default handler being supressed (with e.preventDefault()) in this solution.Zucker
I don't understand what you mean by posting plugins.jquery.com/content/optional-auto-indent-setting, AFAICT this is a feature request, not a solution.Zucker
P
0

This is a lot of borrowed code (thanks, stackoverflow!), with some minor tweaks. The first part is just creating a mirrow, so that you can know what row you are on (not really relevant to your question), but it contains something that sets the current indentation, which is important.

$(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;
  }
})
Pyrethrin answered 5/1, 2015 at 20:52 Comment(1)
Unless I'm missing something, the logic in this answer is faulty. If you type in something like <html> on the first line and hit Enter, your code adds a new line and six spaces (since start is 6). Then type <body> and hit Enter. This adds a new line and 16 spaces. It needs to find the number of spaces before the code begins on the previous line, then add spaces to that.Punak
P
0

Although this post is almost six years old, here's how you can auto-indent textareas:

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

Unfortunately, since it's binded to keyup, while you are holding down enter, the cursor will be at the start of the next line. It will only indent a new line when you release enter. This means that if you tap enter, there will be a delay before it indents:
Auto-indent

Perrins answered 22/1, 2017 at 20:28 Comment(1)
You need to set selectionStart and selectionEnd, otherwise the cursor moves to the end (at least in Chrome). Also, if you type quickly sometimes it doesn't indent, due to the delay.Lialiabilities
O
0

In addition to other people's answers, I would also like to share my attempt to auto-indent the textarea according to the indentation of the previous line of the cursor position.

Here is my method:

1.) Detect on-enter event ( just like what you and other people here did )
2.) Get previous line content relative to cursor position
3.) Get the size of indentation
4.) Force insert spaces / tabs at cursor position

Take a look at the code first and an explanation is provided at the end.

JavaScript code :

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

Explained

var line = this.value.substring(0, this.selectionStart).split("\n");
Extract the text from the beginning to the cursor position and split it by \n into a list.

line = line[line.length - 2]; // string
Get the "last element" of the list , which is the previous line relative to current cursor position. I'm not sure why using line.length - 2 instead of line.length - 1.

var content_to_remove = line.trimStart(); // string
Getting the content to remove ( ie. remove all spaces from the beginning, the rest part is the content to remove ). By getting this , we could use JavaScript string method to replace this with "" , which help us get the indentation of the line :

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

this.setRangeText(indentation, this.selectionStart, this.selectionEnd, "end");
Then we insert the extracted indentation from the previous line to the current line using the setRangeText method. I'm not familiar with this method, so please kindly refer to this answer.


Just in case you want to have a look at the result ( and I don't know how to show the JS part only in the code snippet ) :

  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://mcmap.net/q/75872/-most-elegant-way-to-force-a-textarea-element-to-line-wrap-regardless-of-whitespace */
    /* word-wrap: break-word;
        word-break: break-all;
        text-wrap: unrestricted; */
    /* https://mcmap.net/q/236098/-html-textarea-horizontal-scroll */
    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>
Onagraceous answered 22/6, 2023 at 13:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.