Javascript: Text selection and indentation in textareas

A <textarea> has the properties selectionStart and selectionEnd. If you place your cursor at the first position in the <textarea> they are both 0. If you place the cursor at the second position they are both 1.

If, however, you select text in the <textarea> then selectionStart marks the start and selectionEnd marks the end. We can then grab the text of the selection.

It's worth noting that, if you select to the end of the line, the text selection will include the newline at the end of the line.

var start = textarea.selectionStart
var end = textarea.selectionEnd
var str = value.substring(start, end)

If we have that text we can use a regular expression to add a two spaces at the start the string and at the start of every line. (We look for new lines that have a character after them, thereby ignoring the newline at the end of a line.)

str = str.replace(/^/, "  ").replace(/\n(.)/g, "\n  $1")

If we place a keydown listener on a <textarea>, detect the tab key, disable the default action (tabbing to the next page item), then we can indent the selected text:

your_textarea.addEventListener("keydown", e => {
  var textarea = e.target
  var value = textarea.value
  if(e.keyCode == 9) {
    e.preventDefault()
    var start = textarea.selectionStart
    var end = textarea.selectionEnd
    var str = value.substring(start, end)    
    str = str.replace(/^/, "  ").replace(/\n(.)/g, "\n  $1")
    var before_sel = value.substring(0, start)
    var after_sel = value.substring(end)
    textarea.value = before_sel + str + after_sel 
  }
})

You can play with the current implementation below:


To unident we use shift+tab. We'll change our function and look for a tab or two spaces at the start of a line or at the start of the string and replace such.

your_textarea.addEventListener("keydown", e => {
  var textarea = e.target
  var value = textarea.value
  var shift = e.shiftKey
  if(e.keyCode == 9) {
    e.preventDefault()
    var start = textarea.selectionStart
    var end = textarea.selectionEnd
    var str = value.substring(start, end)
    if(shift) {
      str = str.replace(/^/, "  ")
      .replace(/\n(.)/g, "\n  $1")
    } else {
      str = str.replace(/^(  |\t)/, "")
      .replace(/\n(  |\t)/g, "\n")
    }    
    var before_sel = value.substring(0, start)
    var after_sel = value.substring(end)
    textarea.value = before_sel + str + after_sel 
  }
})

Here's the new implementation:

Our main problem now is the cursor position. Because we added or removed text the start and end position of the cursor will need to change. And that will depend on the amount of text, the number of lines we've changed.

var new_lines = str.match(/\n./g)
new_lines = new_lines ? new_lines.length+1 : 0 

That looks at the number of new lines we have selected. And add one because we add text at the start of the string. And we didn't match a newline there.

Now at the end of our function we need to change the end selection position to either include the newly added whitespace, or exclude the deleted whitespace.

if(shift) {
  textarea.selectionStart = start
  textarea.selectionEnd = end -  (2 * new_lines)
} else {
  textarea.selectionStart = start
  textarea.selectionEnd = end + ( 2 * new_lines)
}

Our final function looks like this:

your_textarea.addEventListener("keydown", e => {
  var textarea = e.target
  var value = textarea.value
  var shift = e.shiftKey
  if(e.keyCode == 9) {
    e.preventDefault()
    var start = textarea.selectionStart
    var end = textarea.selectionEnd
    var str = value.substring(start, end)
    var new_lines = str.match(/\n./g)
    new_lines = new_lines ? new_lines.length+1 : 0 
    if(shift) {
      str = str.replace(/^(  |\t)/, "")
      .replace(/\n(  |\t)/g, "\n")
    } else {
      str = str.replace(/^/, "  ")
      .replace(/\n(.)/g, "\n  $1")
    }    
    var before_sel = value.substring(0, start)
    var after_sel = value.substring(end)
    textarea.value = before_sel + str + after_sel 
    // modify the cursor based on the whitespace added
    if(shift) {
      textarea.selectionStart = start
      textarea.selectionEnd = end -  (2 * new_lines)
    } else {
      textarea.selectionStart = start
      textarea.selectionEnd = end + (2 * new_lines)
    }
  }
})

A problem now is you can't undo in the textarea. And that is achieved using document.executeCommand().

Another is that if the code to reset selectionEnd relies on us having delete two characters for each line, and in the case of a tab this isn't the case.

javascript html
I've removed disqus comments
Instead, press this
to express thanks
So far, 0 people have pressed the octopus