24

My task is to set a text caret to appear inside an empty span node within a contentEditable div.

The following gives me no problems on Firefox 3.6:

<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <script type="text/javascript" src="js/jquery-1.4.3.min.js">
        </script>
        <style>
            #multiple {
                border: 1px solid #ccc;
                width: 800px;
                min-height: 20px;
                padding: 5px;
                outline: none;
            }
        </style>
        <script>
            $(document).ready(function(){
                var contentEditable = document.getElementById('multiple');
                var lastItem = contentEditable.getElementsByTagName('span').item(2);
    
                var selectElementText = function(el, win){
                    win = win || window;
                    var doc = win.document, sel, range;
                    if (win.getSelection && doc.createRange) {                    
                        range = doc.createRange();
                        range.selectNodeContents(el);
                        range.collapse(false);
                        sel = win.getSelection();
                        sel.removeAllRanges();
                        sel.addRange(range);
                    }
                    else 
                        if (doc.body.createTextRange) {
                            range = doc.body.createTextRange();
                            range.moveToElementText(el);
                            range.select();
                        }
                }

                contentEditable.focus();
                selectElementText(lastItem);
            });
        </script>
        <title>Range Selection Tasks (Make Me Want to Cry)</title>
    </head>
    <body>
        <div id="multiple" contentEditable="true">
            <span style="color:red">First</span><span style="color:green">Second</span><span style="color:blue"></span>
        </div>
    </body>
</html>

... but on Webkit and IE, the focus is set to the penultimate span. No idea why. It works if I put a space inside the last span, but then I get a one-character range selection.

Having said that, it's acceptable to have whitespace in the last node if the caret is at the very start.

Any help with this would be greatly appreciated. Thank you in advance.

fcdt
  • 2,371
  • 5
  • 14
  • 26
Mike
  • 253
  • 1
  • 2
  • 7

6 Answers6

15

Set the element's innerHTML to a zero-width character:

('element').innerHTML = '&#200B';

Now the carret can go there.

Dave Jarvis
  • 30,436
  • 41
  • 178
  • 315
shay levi
  • 319
  • 3
  • 11
  • 4
    This worked great; I used `​` in my markup though (`ÈB;` didn't render correctly). – duma Jan 20 '14 at 04:07
  • 6
    This answer mixes up the html and unicode identifier. Use either "​" or "\u200B" to refer to a zero length non-breaking space. – daw May 26 '15 at 22:52
  • Make sure to remove zero-width character once user enters real text. Unless users will notice the existence of zero-width character when they navigate through text using arrow keys. – cipherdragon Dec 03 '20 at 10:46
6

IE's selection/range model is based around indexes into text content, disregarding element boundaries. I believe it may be impossible to set the input focus inside an inline element with no text in it. Certainly with your example I cannot set focus inside the last element by clicking or arrow keys.

It almost works if you set each span to display: block, though there's still some highly strange behaviour, dependent on the existence of whitespace in the parent. Hacking the display to look inline with tricks like float, inline-block and absolute position make IE treat each element as a separate editing box. Relative-positioned block elements next to each other work, but that's probably impractical.

If it makes you feel any better, IE9 finally fixes this unpleasantness and adopts the standard range model. (Hooray!)

it's acceptable to have whitespace in the last node if the caret is at the very start.

I'd probably do that, then, unless an IE selection expert can think of anything better. (Calling Tim Down!)

bobince
  • 528,062
  • 107
  • 651
  • 834
  • Thanks for your prompt and considered answer. You won't believe how many different hacks and workarounds I've attempted while trying to get this to work. Actually, if you have experience of IE, you probably have a fair idea. Good to hear that IE9 finally adopts the standard model. With any luck I should have all my users migrated by 2019... – Mike Oct 31 '10 at 14:51
  • 6
    Hello! I'm pretty certain that it's impossible to place the caret inside an empty inline element in IE. More generally, in IE it's impossible to place the caret at the start of the first text node within an element. This is also true in WebKit: https://bugs.webkit.org/show_bug.cgi?id=15256#c4. Adding whitespace is as good as it gets, I think. I have resorted to inserting a Unicode BOM character (`U+FEFF`, renders as zero width in all major browsers) at the start of a text node, which works but leaves you the logistical nightmare of removing the BOMs when the caret moves elsewhere. – Tim Down Oct 31 '10 at 23:06
  • Indeed. Your guidance and Tim's encouraged me to throw in the towel and just add whitespace programatically. Thank you both. – Mike Nov 02 '10 at 16:33
  • @TimDown Ack! I nearly managed to crack this using `.move()`. I have managed to get a TextRange to *report* that it is in an inline element (when it is at the very start) http://jsfiddle.net/GJLPN/13/ Unfortunately, typing doesn't place text inside the node. Ah well. – Nico Burns Jul 18 '12 at 22:54
  • And it's cracked!! http://jsfiddle.net/GJLPN/31/ Adding a character to the start of the element, using the above move technique, selecting the character using `.moveEnd()`, and then setting `.text` to "" seems to do the trick :). This technique doesn't seem to work with empty inline elements though. – Nico Burns Jul 18 '12 at 23:15
  • @NicoBurns: The caret disappears for me, but typing a character (that does indeed come out bold) brings it back. – Tim Down Jul 18 '12 at 23:31
  • @NicoBurns: Good job on getting this far. Some thoughts: changing the DOM by adding a character may be a bad idea because it breaks the built-in undo stack, although Rangy's TextRange-to-Range algorithm also does this, so I can't really criticise. Doing it by changing the `innerHTML` will break event listeners and any references to descendant nodes so I'd suggest inserting a text node instead. The following seems to have the same effect: http://jsfiddle.net/GJLPN/34/ – Tim Down Jul 18 '12 at 23:49
  • @NicoBurns: I haven't been able to solve the problem of disappearing caret so far. – Tim Down Jul 18 '12 at 23:54
  • @TimDown Yes, inserting a text node seems to work well, and is certainly a cleaner approach. I only used `innerHTML` because I was trying lots of different things, and it's quicker to modify. I have a feeling the disappearing caret may be an unsolvable problem. I tried quite a few things including calling `.focus()` on the element, and any `execCommand` commands that looked like they might do anything. The only thing I can think of is something like a filter which might cause the element to rerender, but I would expect that to remove the caret if anything. – Nico Burns Jul 19 '12 at 13:12
  • In any case, what we have seems to be somewhat helpful, and the same technique also allows for reliably selecting the start of a block element (as opposed to accidentally selecting the end of the previous one), which was the problem I originally had. – Nico Burns Jul 19 '12 at 13:14
  • To fix the disappearing caret, give your element a min width of a couple pixels – Nevir Feb 02 '13 at 01:02
  • @Nevir: Which element? In the example jsFiddles in this thread, the element in question is not empty and changing the min-width has no effect. – Tim Down Apr 04 '13 at 14:52
  • @TimDown U+FEFF works for Chrome\FF but not IE11 - the contenteditable won't take focus. \u200B works for all three. – daw May 26 '15 at 22:46
  • @daw: I favour U+200B these days; I think U+FEFF was only necessary for IE 6 and possibly 7. – Tim Down May 27 '15 at 08:44
  • And, as predicted, I do indeed now have all my users migrated. It only took 8 years... – Mike Jan 11 '19 at 01:23
4

I found a workaround for Webkit, don't know if anybody found this before but instead of programmatically appending a zero-width space, you can do the same thing with the css3 content property in the after psuedo-selector of the elements you want to put the caret in. This has the advantage that the extra characters don't show up in the DOM and the user can't navigate the caret between it. So basically it doesn't need cleaning up.

For this to work for any child element of your content editable element it would be something like this:

#mycontenteditableelement *:after {
    content: '\200B';
}

I didn't check completely, but I suspect this is a full workaround.

bwindels
  • 1,982
  • 16
  • 20
  • 2
    Use at own risk: someone from webkit recommended against using this technique because using the after psuedo-selector is supported badly in contenteditable areas and can cause webkit to crash or the reach an inconsistent state. Again, see webkit bugzilla: bugs.webkit.org/show_bug.cgi?id=15256 – bwindels Dec 06 '10 at 17:40
2

For me setting it content of contenteditable div to <br> works. I tried setting it to nbsp; but that creates extra character space in the div before i start editing.

Hope this helps.

jsbisht
  • 9,079
  • 7
  • 50
  • 55
1

A bit late to the party but I used Unicodes Zero-Width No Break Space (&#65279;) . Then on an event thats called after the users input takes place, remove the Zero-Width No Break Space.

I had many issues with using the Unicode space issue, mostly bad UX. This prevents the user from ever knowing there was an issue.

Sylvia
  • 11
  • 2
0

Applying a minimum height and minimum width to elements inside contenteditable with inline-block can do some good, though the impact has unintended side effects:

#multiple * {
    min-height: 1em;
}
span, b, strong, i, em, a, etc {
    display: inline-block;
    min-width: 1em;
    background-color: hsla(0, 50%, 50%, .5); /* this is only a visual aid; discard @ will */
    vertical-align: middle; /* so elements w/ inline-block align w/ text correctly */ 
}

You'll find there are still issues, but if you're willing to sacrifice some problems for a smaller set of others, this can so some good.

arxpoetica
  • 4,841
  • 3
  • 30
  • 35