user1273587 user1273587 - 6 months ago 10
Javascript Question

Clicking outside a contenteditable div stills give focus to it?

For some reason I need to use contenteditable div instead of normal text input for inputting text. (for some javascript library) It works fine until I found that when I set the contenteditable div using

display: inline-block
, it gives focus to the div even if I click outside the div!

I need it to be giving focus to the div only when the user clicks right onto the div but not around it. Currently, I found that when the user clicks elsewhere and then click at position that is the same row as the div, it gives focus to it.

A simple example to show the problem:

HTML:

<div class="outside">
<div class="text-input" contenteditable="true">
Input 1
</div>
<div class="text-input" contenteditable="true">
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>


CSS:

div.outside {
margin: 30px;
}
div.text-input {
display:inline-block;
background-color: black;
color: white;
width: 300px;
}


The JSFiddle for displaying the problem

Is there a way (CSS or javascript are both acceptable) to make the browser only give focus to div when it is clicked instead of clicking the same row?

P.S. I noticed that there are similar problem (link to other related post), but the situation is a bit different and the solution provided is not working for me.

Answer

Explanation (expanded)

When you click in an editable block, a cursor (a.k.a. insertion point) is placed in the nearest text node that's on the same line as your click-- even if that node is in a child element. You can verify this by clicking around in the tan box in this demo:

.container {width: auto; padding: 30px; background: tan;}
.container * {margin: 4px; padding: 4px;}
div {width: 50%; background: lightgreen;}
span {background: orange;}
span > span {background: gold;}
span > span > span {background: yellow;}
<div class="container" contenteditable>
  text
  <div>
    text in a div
  </div>
  <span><span><span>text in spans</span></span></span></div>
Notice that you can get an insertion point by clicking above the first line or below the last. Some of the proposed solutions don't account for this!

So there's nothing strange about the cursor being placed in a child of the element you clicked on. What is strange is that Webkit browsers (Chrome, Safari, Opera) don't seem to care if the clicked element is editable, as long as the element that the cursor ends up in is. In other words, they're doing this:

on click:
  determine insertion point location;
  if insertion point location is editable:
    add insertion point;

...when they should be doing this:

on click:
  if click location is editable:
    determine insertion point location;
    add insertion point;

I'd consider that a bug.

Block elements don't seem to be affected. That tells me @GOTO 0's answer is onto something in implicating text selection; it does appear to be tied to insertion point placement. What does that have to do with block elements being immune? When you set contenteditable on a block, Webkit disables the ability to select text by multi-clicking outside the block. It's probably no coincidence that insertion point placement via external click is also disabled.


Workaround 1

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

If you must put the contenteditable attribute on the inline-blocks, this solution will allow it. It works by adding uneditable text nodes that take precedence over the inline-blocks when Webkit prematurely sets the insertion point location. (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">&#8203;<div class="text-input" contenteditable>
    Input 1
  </div>&#8203;</span>
  <span class="input-container">&#8203;<div class="text-input" contenteditable>
    Input 2
  </div>&#8203;</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

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>

Comments