I\'m trying to extract the exact selection and cursor location from a textarea. As usual, what\'s easy in most browsers is not in IE.
I\'m using this:
A jquery plugin to get selection index start and end in text area. The above javascript codes didnt work for IE7 and IE8 and gave very inconsistent results, so I have written this small jquery plugin. Allows to temporarily save start and end index of the selection and hightlight the selection at a later time.
A working example and brief version is here: http://jsfiddle.net/hYuzk/3/
A more details version with comments etc. is here: http://jsfiddle.net/hYuzk/4/
// Cross browser plugins to set or get selection/caret position in textarea, input fields etc for IE7,IE8,IE9, FF, Chrome, Safari etc
$.fn.extend({
// Gets or sets a selection or caret position in textarea, input field etc.
// Usage Example: select text from index 2 to 5 --> $('#myTextArea').caretSelection({start: 2, end: 5});
// get selected text or caret position --> $('#myTextArea').caretSelection();
// if start and end positions are the same, caret position will be set instead o fmaking a selection
caretSelection : function(options)
{
if(options && !isNaN(options.start) && !isNaN(options.end))
{
this.setCaretSelection(options);
}
else
{
return this.getCaretSelection();
}
},
setCaretSelection : function(options)
{
var inp = this[0];
if(inp.createTextRange)
{
var selRange = inp.createTextRange();
selRange.collapse(true);
selRange.moveStart('character', options.start);
selRange.moveEnd('character',options.end - options.start);
selRange.select();
}
else if(inp.setSelectionRange)
{
inp.focus();
inp.setSelectionRange(options.start, options.end);
}
},
getCaretSelection: function()
{
var inp = this[0], start = 0, end = 0;
if(!isNaN(inp.selectionStart))
{
start = inp.selectionStart;
end = inp.selectionEnd;
}
else if( inp.createTextRange )
{
var inpTxtLen = inp.value.length, jqueryTxtLen = this.val().length;
var inpRange = inp.createTextRange(), collapsedRange = inp.createTextRange();
inpRange.moveToBookmark(document.selection.createRange().getBookmark());
collapsedRange.collapse(false);
start = inpRange.compareEndPoints('StartToEnd', collapsedRange) > -1 ? jqueryTxtLen : inpRange.moveStart('character', -inpTxtLen);
end = inpRange.compareEndPoints('EndToEnd', collapsedRange) > -1 ? jqueryTxtLen : inpRange.moveEnd('character', -inpTxtLen);
}
return {start: Math.abs(start), end: Math.abs(end)};
},
// Usage: $('#txtArea').replaceCaretSelection({start: startIndex, end: endIndex, text: 'text to replace with', insPos: 'before|after|select'})
// Options start: start index of the text to be replaced
// end: end index of the text to be replaced
// text: text to replace the selection with
// insPos: indicates whether to place the caret 'before' or 'after' the replacement text, 'select' will select the replacement text
replaceCaretSelection: function(options)
{
var pos = this.caretSelection();
this.val( this.val().substring(0,pos.start) + options.text + this.val().substring(pos.end) );
if(options.insPos == 'before')
{
this.caretSelection({start: pos.start, end: pos.start});
}
else if( options.insPos == 'after' )
{
this.caretSelection({start: pos.start + options.text.length, end: pos.start + options.text.length});
}
else if( options.insPos == 'select' )
{
this.caretSelection({start: pos.start, end: pos.start + options.text.length});
}
}
});
N.B. Please refer to my other answer for the best solution I can offer. I'm leaving this here for background.
I've come across this problem and written the following that works in all cases. In IE it does use the method you suggested of temporarily inserting a character at the selection boundary, and then uses document.execCommand("undo")
to remove the inserted character and prevent the insertion from remaining on the undo stack. I'm pretty sure there's no easier way. Happily, IE 9 will support the selectionStart
and selectionEnd
properties.
function getSelectionBoundary(el, isStart) {
var property = isStart ? "selectionStart" : "selectionEnd";
var originalValue, textInputRange, precedingRange, pos, bookmark;
if (typeof el[property] == "number") {
return el[property];
} else if (document.selection && document.selection.createRange) {
el.focus();
var range = document.selection.createRange();
if (range) {
range.collapse(!!isStart);
originalValue = el.value;
textInputRange = el.createTextRange();
precedingRange = textInputRange.duplicate();
pos = 0;
if (originalValue.indexOf("\r\n") > -1) {
// Trickier case where input value contains line breaks
// Insert a character in the text input range and use that as
// a marker
range.text = " ";
bookmark = range.getBookmark();
textInputRange.moveToBookmark(bookmark);
precedingRange.setEndPoint("EndToStart", textInputRange);
pos = precedingRange.text.length - 1;
// Executing an undo command to delete the character inserted
// prevents this method adding to the undo stack. This trick
// came from a user called Trenda on MSDN:
// http://msdn.microsoft.com/en-us/library/ms534676%28VS.85%29.aspx
document.execCommand("undo");
} else {
// Easier case where input value contains no line breaks
bookmark = range.getBookmark();
textInputRange.moveToBookmark(bookmark);
precedingRange.setEndPoint("EndToStart", textInputRange);
pos = precedingRange.text.length;
}
return pos;
}
}
return 0;
}
var el = document.getElementById("your_textarea");
var startPos = getSelectionBoundary(el, true);
var endPos = getSelectionBoundary(el, false);
alert(startPos + ", " + endPos);
UPDATE
Based on bobince's suggested approach in the comments, I've created the following, which seems to work well. Some notes:
function getSelection(el) {
var start = 0, end = 0, normalizedValue, textInputRange, elStart;
var range = document.selection.createRange();
var bigNum = -1e8;
if (range && range.parentElement() == el) {
normalizedValue = el.value.replace(/\r\n/g, "\n");
start = -range.moveStart("character", bigNum);
end = -range.moveEnd("character", bigNum);
textInputRange = el.createTextRange();
range.moveToBookmark(textInputRange.getBookmark());
elStart = range.moveStart("character", bigNum);
// Adjust the position to be relative to the start of the input
start += elStart;
end += elStart;
// Correct for line breaks so that offsets are relative to the
// actual value of the input
start += normalizedValue.slice(0, start).split("\n").length - 1;
end += normalizedValue.slice(0, end).split("\n").length - 1;
}
return {
start: start,
end: end
};
}
var el = document.getElementById("your_textarea");
var sel = getSelection(el);
alert(sel.start + ", " + sel.end);
I'm adding another answer since my previous one is already getting somewhat epic.
This is what I consider the best version yet: it takes bobince's approach (mentioned in the comments to my first answer) and fixes the two things I didn't like about it, which were first that it relies on TextRanges that stray outside the textarea (thus harming performance), and second the dirtiness of having to pick a giant number for the number of characters to move the range boundary.
function getSelection(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
};
}
var el = document.getElementById("your_textarea");
var sel = getSelection(el);
alert(sel.start + ", " + sel.end);
The move by negative bazillion seems to work perfectly.
Here's what I ended up with:
var sel=document.selection.createRange();
var temp=sel.duplicate();
temp.moveToElementText(textarea);
var basepos=-temp.moveStart('character', -10000000);
this.m_selectionStart = -sel.moveStart('character', -10000000)-basepos;
this.m_selectionEnd = -sel.moveEnd('character', -10000000)-basepos;
this.m_text=textarea.value.replace(/\r\n/gm,"\n");
Thanks bobince - how can I vote up your answer when it's just a comment :(