views:

4441

answers:

6

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 -&gt;
            <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?

A: 

I recently worked at a site which used Microsoft CMS with the "MSIB+ pack" of controls which included a WYSIWYG editor which ran in Internet Explorer.

I seem to remember some comments in the editor client-side Javascript which were specifically related to this bug in IE and the Range.Select() method.

Unfortunately, I'm not working there anymore so I can't access the Javascript files, but perhaps you may be able to get them from elsewhere?

Good luck

Dave R
Do you know if the WYSIWYG editor had a name? Might make it easier for searching...
Douglas Mayle
I've managed to get a copy of the MSIB+ pack, but I can't seem to find the WYSIWYG editor inside. I'm not sure if the copy I have is out of date, or whther the editor is actually a part of CMS editor...
Douglas Mayle
A: 

This is a known issue. Check out jquery.

Unless you're referring to a plugin, I think you're mistaken. There's no code in jquery that deals with selections.
Douglas Mayle
What Douglas said. -1.
Triptych
A: 

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.

Alconja
Thanks for taking a look. I'm know it's a bug in IE, I'm just looking for a safe workaround because I do need to do move the selection around before restoring it.
Douglas Mayle
+5  A: 

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.

Dan Eisenberg
A: 

Maybe I misunderstand, but if I click to the right of that first line, my cursor already immediately appears at the start of the second line, so that's not a TextRange issue, right?

Adding a doctype so the test page is rendered in standards mode instead of quirks mode fixes that for me.

And I can't reproduce what your myhandler function does, because clicking that button moves the focus away to the button so I can no longer see the cursor and can't get it back either. Finding the cursor position in a contentEditable area seems to be a different problem altogether.

mercator
A: 

Douglas,

Did you manage to find a work around for your problem? I am experiencing the same issue.

robin