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