Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 740
cytsunny Avatar asked Dec 18 '15 10:12

cytsunny


People also ask

How do you focus on Contenteditable?

Changing your tabIndex to >= 0 will let you focus on the elements. If you need to do it dynamically, you can just add a tabindex >= 0 to your element in the event listener.

How do I make a div editable on click?

Answer: Use the HTML5 contenteditable Attribute You can set the HTML5 contenteditable attribute with the value true (i.e. contentEditable="true" ) to make an element editable in HTML, such as <div> or <p> element.

How does Contenteditable work?

The contenteditable attribute specifies whether the content of an element is editable or not. Note: When the contenteditable attribute is not set on an element, the element will inherit it from its parent.

What is the function of Contenteditable attribute?

The contenteditable attribute in HTML is used to set whether the content is editable or not using boolean values true or false. This attribute can be used with any element since it is a Global Attribute.


2 Answers

I was able to reproduce this behavior only in Chrome and Safari, suggesting that this may be a Webkit related issue.

It's hard to tell what's going on without inspecting the code but we can at least suspect that the problem lies in some faulty mechanism that triggers text selection in the browser. For analogy, if the divs were not contenteditable, clicking in the same line of text after the last character would trigger a text selection starting at the end of the line.

The workaround is to wrap the contenteditable divs into a container element and style the container with -webkit-user-select: none to make it unselectable.

As Alex Char points out in a comment, this will not prevent a mouse click outside the container to trigger a selection at the start of the text inside it, since there is no static text between the first contenteditable div and the (selectable) ancestor container around it. There are likely more elegant solutions, but a way to overcome this problem is to insert an invisible, nonempty span of text of zero width just before the first contenteditable div to capture the unwanted text selection.

  • Why non empty?: Because empty elements are ignored upon text selection.
  • Why zero width?: Because we don't want to see it...
  • Why invisible?: Because we don't want the content to be copied to the clipboard with, say Ctrl+A, Ctrl+C.

div.outside {    margin: 30px;  }  div.text-input {    display:inline-block;    background-color: black;    color: white;    width: 300px;  }  div.text-input-container {    -webkit-user-select: none;  }  .invisible {    visibility: hidden;  }
<div class="outside">      <div class="text-input-container">          <span class="invisible">&#8203;</span><div class="text-input" contenteditable="true">              Input 1          </div>          <div class="text-input" contenteditable="true">              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>

Even in normal circumstances it is generally a good idea to keep adjacent inline-block elements in a separate container rather than next to a block element (like the unrelated div) to prevent unexpected layout effects in case the order of the sibling elements changes.

like image 37
GOTO 0 Avatar answered Oct 12 '22 15:10

GOTO 0


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">&#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 (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>
like image 90
DoctorDestructo Avatar answered Oct 12 '22 13:10

DoctorDestructo