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:
- bobince's approach is simpler and shorter.
- My approach is intrusive: it makes changes to the input's value before reverting those changes, although there is no visible effect of this.
- My approach has the advantage of keeping all operations within the input. bobince's approach relies on creating ranges that span from the start of the body to the current selection.
- A consequence of 3. is that the performance of bobince's varies with the position of the input within the document whereas mine does not. My simple tests suggest that when the input is close to the start of the document, bobince's approach is significantly faster. When the input is after a significant chunk of HTML, my approach is faster.
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);