21

I have an html textarea that will be updated periodically via javascript.

when I do this:

$("#textarea").val(new_val); 

The cursor moves to the end of the text.

I would like to update the text without changing the cursor position. Also, if the user has a range of text selected, the highlight should be preserved.

3 Answers 3

24

Here is a pair of functions that get and set the selection/caret position in a text area in all major browsers.

Note: if you don't need to support IE <= 8, just use the selectionStart and selectionEnd properties (MDN). All of the complicated code below is just there to support old versions of IE.

function getInputSelection(el) { var start = 0, end = 0, normalizedValue, range, textInputRange, len, endRange; if (typeof el.selectionStart == "number" && typeof el.selectionEnd == "number") { start = el.selectionStart; end = el.selectionEnd; } else { range = document.selection.createRange(); if (range && range.parentElement() == el) { len = el.value.length; normalizedValue = el.value.replace(/\r\n/g, "\n"); // Create a working TextRange that lives only in the input textInputRange = el.createTextRange(); textInputRange.moveToBookmark(range.getBookmark()); // Check if the start and end of the selection are at the very end // of the input, since moveStart/moveEnd doesn't return what we want // in those cases endRange = el.createTextRange(); endRange.collapse(false); if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) { start = end = len; } else { start = -textInputRange.moveStart("character", -len); start += normalizedValue.slice(0, start).split("\n").length - 1; if (textInputRange.compareEndPoints("EndToEnd", endRange) > -1) { end = len; } else { end = -textInputRange.moveEnd("character", -len); end += normalizedValue.slice(0, end).split("\n").length - 1; } } } } return { start: start, end: end }; } function offsetToRangeCharacterMove(el, offset) { return offset - (el.value.slice(0, offset).split("\r\n").length - 1); } function setInputSelection(el, startOffset, endOffset) { if (typeof el.selectionStart == "number" && typeof el.selectionEnd == "number") { el.selectionStart = startOffset; el.selectionEnd = endOffset; } else { var range = el.createTextRange(); var startCharMove = offsetToRangeCharacterMove(el, startOffset); range.collapse(true); if (startOffset == endOffset) { range.move("character", startCharMove); } else { range.moveEnd("character", offsetToRangeCharacterMove(el, endOffset)); range.moveStart("character", startCharMove); } range.select(); } } 

When you change the textarea's value, first save the selection, then restore it afterwards:

var t = document.getElementById("textarea"); var sel = getInputSelection(t); t.value = some_new_value; setInputSelection(t, sel.start, sel.end); 
Sign up to request clarification or add additional context in comments.

5 Comments

Does the setInputSelection function work only in IE? I'm asking because the createTextRange method does not exist (for input elements) in the other browsers, so that an error is thrown.
I created a demo using your code. The code is here: vidasp.net/js/selection.js and the demo is here: vidasp.net/tinydemos/select-demo.html The demo works flawlessly in IE9 beta, Chrome and Safari. However, there is an issue in Firefox: If you set the selection programmatically ( selec.set(input, 10, 20); it wont work unless you focus the text-box ( input.focus(); ). In my demo, I am focusing the text-box after setting the selection - just to make it work in Firefox. Therefore, consider placing el.focus() at the end of the setInputSelection function.
Is this still the best way to handle this? I came across this code in an older project and it seems really long/complicated for such a seemingly simple task
@Marie: The complication is just there for IE <= 8. You can lose nearly all of it and just use selectionStart and selectionEnd if you don't need to support IE 8.
One note - typeof NaN === 'number' comes out to true. This happens to not hit that boundary case, but in case anyone messes around with the code, they should know that it'd be stronger to handle that case.
0

A decade later but this is what I came up with for replacing items in a textarea. Some additional handling is needed to adjust the caret or selection when replacing with longer or shorter text.

// find and replace in textarea while preserving caret and selection function replaceText(el, findText, replaceWithText) { var text = el.value; var selectionStart = 0; var selectionEnd = 0; // only support modern browsers for preserving caret and selection if (el.setSelectionRange) { selectionStart = el.selectionStart; selectionEnd = el.selectionEnd; } var start = 0; while ((start = text.indexOf(findText, start)) > -1) { var end = start + findText.length; text = text.substr(0, start) + replaceWithText + text.substr(end); if (selectionStart < end) { selectionStart = Math.min(selectionStart, start + replaceWithText.length); } else { selectionStart = selectionStart + replaceWithText.length - (end - start); } if (selectionEnd < end) { selectionEnd = Math.min(selectionEnd, start + replaceWithText.length); } else { selectionEnd = selectionEnd + replaceWithText.length - (end - start); } start += replaceWithText.length; } // don't do anything unless we need to (otherwise destroys undo) if (el.value != text) { el.value = text; if (el.setSelectionRange) { el.selectionStart = selectionStart; el.selectionEnd = selectionEnd; } } }
Place caret on or after the word LONGER, or select some text after or including it: <br /> <textarea id='t'>Here is some LONGERtext to replace</textarea> <br /> <input type="button" onclick="replaceText(document.getElementById('t'),'LONGER',''); document.getElementById('t').focus();" value="remove word LONGER" />

Comments

0

With Java-Diff-Utils one can update the text and have the cursor position adapted "intelligently".

import com.github.difflib.DiffUtils; import com.github.difflib.patch.AbstractDelta; private void setTextAndUpdateCaretPosition(TextInputControl textInputControl, String newText) { int lastCaretPosition = textInputControl.getCaretPosition(); String oldText = textInputControl.getText(); textInputControl.setText(newText); if (oldText == null) { return; } if (newText == null) { return; } if (oldText.equals(newText)) { return; } // This is a special case when the text is set to a new value // In this case, we want to adjust the caret position List<String> oldValueCharacters = Arrays.asList(oldText.split("")); List<String> newValueCharacters = Arrays.asList(newText.split("")); List<AbstractDelta<String>> deltaList = DiffUtils.diff(oldValueCharacters, newValueCharacters).getDeltas(); AbstractDelta<String> lastDelta = null; for (AbstractDelta<String> delta : deltaList) { if (delta.getSource().getPosition() > lastCaretPosition) { break; } lastDelta = delta; } if (lastDelta == null) { // Change happened after current caret position // Thus, simply restore the old position textInputControl.positionCaret(lastCaretPosition); } else { int offset = lastDelta.getTarget().getPosition() - lastDelta.getSource().getPosition(); switch (lastDelta.getType()) { case DELETE: offset -= lastDelta.getSource().size(); break; case INSERT: offset += lastDelta.getTarget().size(); break; case CHANGE: offset += lastDelta.getTarget().size() - lastDelta.getSource().size(); break; default: break; } int newCaretPosition = lastCaretPosition + offset; textInputControl.positionCaret(newCaretPosition); } } 

Full source in real world application: https://github.com/JabRef/jabref/blob/3401b71fffeca4178ba8ffabadfac7a65a737915/src/main/java/org/jabref/gui/fieldeditors/FieldEditorFX.java#L41

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.