Clicking outside a contenteditable div stills give focus to it?

后端 未结 5 1224
独厮守ぢ
独厮守ぢ 2020-12-14 15:52

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

相关标签:
5条回答
  • 2020-12-14 16:14

    How about a little jQuery?

    $(".outside").click(function(e){
        $(e.target).siblings(".text-input").blur();
        window.getSelection().removeAllRanges();
    });
    

    And if IRL you need to account for clicks on contenteditable=true siblings' children:

    $(".outside").click(function(e){
        if ($(e.target).siblings(".text-input").length != 0){
            $(e.target).siblings(".text-input").blur();
            window.getSelection().removeAllRanges();
        } 
        else {
            $(e.target).parentsUntil(".outside").last().siblings(".text-input").blur();
            window.getSelection().removeAllRanges();
        }
    });
    

    window.getSelection().removeAllRanges();"The trick is to remove all ranges after calling blur"

    0 讨论(0)
  • 2020-12-14 16:15

    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>

    0 讨论(0)
  • 2020-12-14 16:16

    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.

    0 讨论(0)
  • 2020-12-14 16:22

    Disable text selection in container... should fix that.

    For example:

    * {
       -ms-user-select: none; /* IE 10+ */
       -moz-user-select: -moz-none;
       -khtml-user-select: none;
       -webkit-user-select: none;
       user-select: none;
    }
    
    0 讨论(0)
  • 2020-12-14 16:31

    If it's not needed to use display: inline-block, I would recommend using float. Here is the example.

    Based on your example, the new CSS would be:

    div.text-input {
      display: block;
      background-color: black;
      color: white;
      width: 300px;
      float: left;
      margin-right: 10px;
    }
    div.unrelated {
      clear: both;
    }
    
    0 讨论(0)
提交回复
热议问题