Explanation (if you don't care, skip to the Workarounds below)
When you click in an editable element, the browser places a cursor (a.k.a. insertion point) in the nearest text node that is within the clicked element, on the same line as your click. The text node may be either directly within the clicked element, or in one of its child elements. You can verify this by running the code snippet below and clicking around in the large blue box.
.container {width: auto; padding: 20px; background: cornflowerblue;}
.container * {margin: 4px; padding: 4px;}
div {width: 50%; background: gold;}
span {background: orange;}
span > span {background: gold;}
span > span > span {background: yellow;}
<div class="container" contenteditable>
text in an editable element
<div>
text in a nested div
</div>
<span><span><span>text in a deeply nested span</span></span></span></div>
Notice that you can get an insertion point by clicking above the first line or below the last. This is because the "hitbox" of these lines extends to the top and bottom of the container, respectively. Some of the other answers don't account for this!
The blue box is a <div>
with the contenteditable
attribute, and the inner orange/yellow boxes are nested child elements. Notice that if you click near (but not in) one of the child elements, the cursor ends up inside it, even though you clicked outside. This is not a bug. Since the element you clicked on (the blue box) is editable and the child element is part of its content, it makes sense to place the cursor in the child element if that's where the nearest text node happens to be.
The problem is that Webkit browsers (Chrome, Safari, Opera) exhibit this same behavior when contenteditable
is set on the child instead of the parent. The browser shouldn't even bother looking for the nearest text node in this case since the element you actually clicked on isn't editable. But Webkit does, and if that text node happens to be in the editable child, you get a blinking cursor. I'd consider that a bug; Webkit browsers are doing this:
on click:
find nearest text node within clicked element;
if text node is editable:
add insertion point;
...when they should be doing this:
on click:
if clicked element is editable:
find nearest text node within clicked element;
add insertion point;
Block elements (such as divs) don't seem to be affected by the bug, which makes me think @GOTO 0's answer is correct in implicating text selection-- at least insofar as it seems to be governed by the same logic that controls insertion point placement. Multi-clicking outside an inline element highlights the text within it, but not so for block elements. It's probably no coincidence that you also don't get an insertion point when you click outside a block. The first workaround below makes use of this exception.
Workaround 1 (nested div)
Since blocks aren't affected by the bug, I think the best solution is to nest a div in the inline-block and make it editable instead. Inline-blocks already behave like blocks internally, so the div should have no effect on its behavior.
div.outside {
margin: 30px;
}
div.text-input {
display:inline-block;
background-color: black;
color: white;
width: 300px;
}
<div class="outside">
<div class="text-input">
<div contenteditable>
Input 1
</div>
</div>
<div class="text-input">
<div contenteditable>
Input 2
</div>
</div>
<div class="unrelated">This is some unrelated content<br>
This is some more unrelated content
This is just some space to shows that clicking here doesn't mess with the contenteditable div
but clicking the side mess with it.
</div>
</div>
Workaround 2 (invisible characters)
If you must put the contenteditable
attribute on the inline-blocks, this solution will allow it. It works by surrounding the inline-blocks with invisible characters (specifically, zero-width spaces) which shield them from external clicks. (GOTO 0's answer uses the same principle, but it still had some problems last I checked).
div.outside {
margin: 30px;
}
div.text-input {
display:inline-block;
background-color: black;
color: white;
width: 300px;
white-space: normal;
}
.input-container {white-space: nowrap;}
<div class="outside">
<span class="input-container">​<div class="text-input" contenteditable>
Input 1
</div>​</span>
<span class="input-container">​<div class="text-input" contenteditable>
Input 2
</div>​</span>
<div class="unrelated">This is some unrelated content<br>
This is some more unrelated content
This is just some space to shows that clicking here doesn't mess with the contenteditable div
but clicking the side mess with it.
</div>
</div>
Workaround 3 (javascript)
If you absolutely can't change your markup, then this JavaScript-based solution could work as a last resort (inspired by this answer). It sets contentEditable
to true when the inline-blocks are clicked, and false when they lose focus.
(function() {
var inputs = document.querySelectorAll('.text-input');
for(var i = inputs.length; i--;) {
inputs[i].addEventListener('click', function(e) {
e.target.contentEditable = true;
e.target.focus();
});
inputs[i].addEventListener('blur', function(e) {
e.target.contentEditable = false;
});
}
})();
div.outside {
margin: 30px;
}
div.text-input {
display:inline-block;
background-color: black;
color: white;
width: 300px;
}
<div class="outside">
<div class="text-input">
Input 1
</div>
<div class="text-input">
Input 2
</div>
<div class="unrelated">This is some unrelated content<br>
This is some more unrelated content
This is just some space to shows that clicking here doesn't mess with the contenteditable div
but clicking the side mess with it.
</div>
</div>