Explanation (if this doesn't interest you, feel free to skip to the Workarounds below)
Upon clicking on an editable element, the browser positions a cursor (referred to as an insertion point) in the closest text node within the clicked element, at the same vertical level as where you clicked. This text node might be directly within the clicked element or nested inside one of its child elements. You can experiment with this behavior by running the code snippet provided below and interacting within the large blue box.
The blue box represents a <div>
with the contenteditable
attribute, while the inner orange/yellow boxes denote nested child elements. Note that if you click close to but not inside one of the child elements, the cursor will still end up inside it, even if your click was outside. Though counterintuitive, this is not a glitch. Given that the element clicked (the blue box) is editable and the child element is part of its content, it makes sense for the cursor to be placed in the child element if that's where the nearest text node lies.
The issue arises when Webkit browsers (Chrome, Safari, Opera) exhibit similar behavior even when the contenteditable
attribute is set on the child rather than the parent. In such cases, the browser erroneously searches for the nearest text node within the editable child, resulting in a blinking cursor. From my perspective, this behavior qualifies as a bug; Webkit browsers are executing the following logic:
on click:
find nearest text node within clicked element;
if text node is editable:
add insertion point;
...when they should ideally adhere to this approach:
on click:
if clicked element is editable:
find nearest text node within clicked element;
add insertion point;
Interestingly, block elements like divs do not seem to encounter this bug, leading me to agree with @GOTO 0's answer, which implicates text selection being governed by the same logic dictating insertion point placement. Clicking multiple times outside an inline element highlights the enclosed text, a behavior absent for block elements. It appears there is a correlation between the absence of an insertion point upon clicking outside a block and this different highlighting behavior. The first workaround listed below leverages this exception.
Workaround 1 (nested div)
To circumvent this issue, considering blocks are immune to the bug, embedding a div within the inline-block and assigning it the editable property seems like the most effective solution. Since inline-blocks internally behave akin to block elements, introducing a nested div shouldn’t impact their behavior.
.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>
Note how an insertion point can be attained by clicking above the first line or below the last due to the "hitbox" extension to the top and bottom of the container, respectively. Some other solutions fail to address this aspect!
Workaround 2 (invisible characters)
If retaining the contenteditable
attribute on the inline-blocks is unavoidable, this workaround offers a feasible approach. By enveloping the inline-blocks with invisible characters (such as zero-width spaces), external clicks are shielded from affecting them. (GOTO 0's answer also utilizes this principle, though some issues were noted during recent evaluations).
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)
In scenarios mandating no changes to the markup structure, resorting to a JavaScript-driven solution could serve as a final recourse (inspired by this response). This solution involves toggling the contentEditable
state to true upon clicking on the inline-blocks, reverting it back to false once focus is lost.
(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 show that clicking here doesn't affect the contenteditable div
but clicking the side does.
</div>
</div>