I'm having an unusual problem with an IE document with contentEditable set to true. Calling select() on a range that is positioned at the end of a text node that immediately precedes a block element causes the selection to be shifted to the right one character and appear where it shouldn't. I've submitted a bug to Microsoft against IE8. If you can, please vote for this issue so that it can be fixed.
https://connect.microsoft.com/IE/feedback/ViewFeedback.aspx?FeedbackID=390995
I've written a test case to demonstrate the effect:
<html>
<body>
<iframe id="editable">
<html>
<body>
<div id="test">
Click to the right of this line ->
<p id="par">Block Element</p>
</div>
</body>
</html>
</iframe>
<input id="mytrigger" type="button" value="Then Click here to Save and Restore" />
<script type="text/javascript">
window.onload = function() {
var iframe = document.getElementById('editable');
var doc = iframe.contentDocument || iframe.contentWindow.document;
// An IFRAME without a source points to a blank document. Here we'll
// copy the content we stored in between the IFRAME tags into that
// document. It's a hack to allow us to use only one HTML file for this
// test.
doc.body.innerHTML = iframe.textContent || iframe.innerHTML;
// Marke the IFRAME as an editable document
if (doc.body.contentEditable) {
doc.body.contentEditable = true;
} else {
var mydoc = doc;
doc.designMode = 'On';
}
// A function to demonstrate the bug.
var myhandler = function() {
// Step 1 Get the current selection
var selection = doc.selection || iframe.contentWindow.getSelection();
var range = selection.createRange ? selection.createRange() : selection.getRangeAt(0);
// Step 2 Restore the selection
if (range.select) {
range.select();
} else {
selection.removeAllRanges();
selection.addRange(range);
doc.body.focus();
}
}
// Set up the button to perform the test code.
var button = document.getElementById('mytrigger');
if (button.addEventListener) {
button.addEventListener('click', myhandler, false);
} else {
button.attachEvent('onclick', myhandler);
}
}
</script>
</body>
</html>
The problem is exposed in the myhandler function. This is all that I'm doing, there is no Step 3 in between the saving and restoring the selection, and yet the cursor moves. It doesn't seem to happen unless the selection is empty (ie. I have a blinking cursor, but no text), and it only seems to happen whenever the cursor is at the end of a text node that immediately precedes a block node.
It seems that the range is still in the correct position (if I call parentElement on the range it returns the div), but if I get a new range from the current selection, the new range is inside the paragraph tag, and that is its parentElement.
How do I work around this and consistently save and restore the selection in internet explorer?
I've figured out a few methods for dealing with IE ranges like this.
If all you want to do is save where the cursor is, and then restore it, you can use the pasteHTML method to insert an empty span at the current position of the cursor, and then use the moveToElementText method to put it back at that position again:
// Save position of cursor
range.pasteHTML('<span id="caret"></span>')
...
// Create new cursor and put it in the old position
var caretSpan = iframe.contentWindow.document.getElementById("caret");
var selection = iframe.contentWindow.document.selection;
newRange = selection.createRange();
newRange.moveToElementText(caretSpan);
Alternatively, you can count how many characters precede the current cursor position and save that number:
var selection = iframe.contentWindow.document.selection;
var range = selection.createRange().duplicate();
range.moveStart('sentence', -1000000);
var cursorPosition = range.text.length;
To restore the cursor, you set it to the beginning and then move it that number of characters:
var newRange = selection.createRange();
newRange.move('sentence', -1000000);
newRange.move('character', cursorPosition);
Hope this helps.
I've had a bit of a dig & unfortunately can't see a workaround... Although one thing I noticed while debugging the javascript, it seems like the problem is actually with the range object itself rather than with the subsequent range.select()
. If you look at the values on the range object that selection.createRange()
returns, yes the parent dom object may be correct, but the positioning info is already referring to the start of the next line (i.e. offsetLeft/Top, boundingLeft/Top, etc are already wrong).
Based on the info here, here and here, I think you're out of luck with IE, since you only have access to Microsoft's TextRange object, which appears to be broken. From my experimentation, you can move the range around and position it exactly where it should be, but once you do so, the range object automatically shifts down to the next line even before you've tried to .select()
it. For example, you can see the problem by putting this code in between your Step 1 & Step 2:
if (range.boundingWidth == 0)
{
//looks like its already at the start of the next line down...
alert('default position: ' + range.offsetLeft + ', ' + range.offsetTop);
//lets move the start of the range one character back
//(i.e. select the last char on the line)
range.moveStart("character", -1);
//now the range looks good (except that its width will be one char);
alert('one char back: ' + range.offsetLeft + ', ' + range.offsetTop);
//calculate the true end of the line...
var left = range.offsetLeft + range.boundingWidth;
var top = range.offsetTop;
//now we can collapse back down to 0 width range
range.collapse();
//the position looks right
alert('moving to: ' + left + ', ' + top);
//move there.
range.moveToPoint(left, top);
//oops... on the next line again... stupid IE.
alert('moved to: ' + range.offsetLeft + ', ' + range.offsetTop);
}
So, unfortunately it doesn't look like there's any way to ensure that the range is in the right spot when you select it again.
Obviously there's the trivial fix to your code above, by changing Step 2 it to this:
// Step 2 Restore the selection
if (range.select) {
if (range.boundingWidth > 0) {
range.select();
}
} else {
selection.removeAllRanges();
selection.addRange(range);
doc.body.focus();
}
But presumably, you actually want to do something between Step 1 & Step 2 in your actual which involves moving the selection, hence the need to re-set it. But just in case. :)
So, the best I can do is go & vote for the bug you created... Hopefully they'll fix it.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With