21

When you hit Enter on a contentEditable element every browser is handling the resulting code differently: Firefox inserts a BR tag, Chrome inserts a DIV tag while Internet Explorer inserts a P tag.

I was desperately looking for a solution to at least use a BR or P for all browsers and the most common answer was this:

inserting BR tag :

$("#editableElement").on("keypress", function(e){
      if (e.which == 13) {
        if (window.getSelection) {
          var selection = window.getSelection(),
              range = selection.getRangeAt(0),
              br = document.createElement("br");
          range.deleteContents();
          range.insertNode(br);
          range.setStartAfter(br);
          range.setEndAfter(br);
          selection.removeAllRanges();
          selection.addRange(range);
          return false;
        }
      }
    });

But this doesn't work because it seems that browsers don't know how to set the caret after <br> which means the following is not doing anything useful (especially if you hit enter when the caret is placed at the end of text):

range.setStartAfter(br);
range.setEndAfter(br);

Some people would say: use double <br><br> but this results in two line breaks when you hit enter inside a text node.

Others would say always add an additional <br> at the end of contentEditable, but if you have a <div contenteditable><p>text here</p></div> and you place the cursor at the end of text then hit enter, you will get the wrong behavior.

So I said to myself maybe we can use P instead of BR, and the common answer is:

inserting P tag:

document.execCommand('formatBlock', false, 'p');

But this doesn't work consistently either.

As you can see, all these solutions leave something to be desired. Is there another solution that solves this issue?

Peter O.
  • 32,158
  • 14
  • 82
  • 96
medBouzid
  • 7,484
  • 10
  • 56
  • 86

2 Answers2

11

One possible solution: append a text node with a zero-width space character after the <br> element. This is a non-printing zero-width character that's specifically designed to:

...indicate word boundaries to text processing systems when using scripts that do not use explicit spacing, or after characters (such as the slash) that are not followed by a visible space but after which there may nevertheless be a line break.

(Wikipedia)

Tested in Chrome 48, Firefox 43, and IE11.

$("#editableElement").on("keypress", function(e) {
  //if the last character is a zero-width space, remove it
  var contentEditableHTML = $("#editableElement").html();
  var lastCharCode = contentEditableHTML.charCodeAt(contentEditableHTML.length - 1);
  if (lastCharCode == 8203) {
    $("#editableElement").html(contentEditableHTML.slice(0, -1));
  }
  // handle "Enter" keypress
  if (e.which == 13) {
    if (window.getSelection) {
      var selection = window.getSelection();
      var range = selection.getRangeAt(0);
      var br = document.createElement("br");
      var zwsp = document.createTextNode("\u200B");
      var textNodeParent = document.getSelection().anchorNode.parentNode;
      var inSpan = textNodeParent.nodeName == "SPAN";
      var span = document.createElement("span");
      
      // if the carat is inside a <span>, move it out of the <span> tag
      if (inSpan) {
        range.setStartAfter(textNodeParent);
        range.setEndAfter(textNodeParent);
      }

      // insert the <br>
      range.deleteContents();
      range.insertNode(br);
      range.setStartAfter(br);
      range.setEndAfter(br);
      
      // create a new span on the next line
      if (inSpan) {
        range.insertNode(span);
        range.setStart(span, 0);
        range.setEnd(span, 0);
      }

      // add a zero-width character
      range.insertNode(zwsp);
      range.setStartBefore(zwsp);
      range.setEndBefore(zwsp);
      
      // insert the new range
      selection.removeAllRanges();
      selection.addRange(range);
      return false;
    }
  }
});
#editableElement {
  height: 150px;
  width: 500px;
  border: 1px solid black;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div contenteditable=true id="editableElement">
  <span>sample text</span>
</div>
rphv
  • 5,409
  • 3
  • 29
  • 47
  • Very cool. Up till now I've used an almost identical approach, but with a space instead of a ZWNJ. This of course required a check that the injected `
    ` is the last non-empty node in the parent `contenteditable`, otherwise visible spaces were left all over the place. This is much better.
    – Igor Raush Mar 03 '16 at 01:16
  • thank you for the answer, Is there a way to delete the previous added ‌ in each enter keypress ? – medBouzid Mar 03 '16 at 11:28
  • Sure, but you'll need to wait until another character is placed in the `TextNode`, otherwise you're back where you started (with the cursor placed relative to an empty `TextNode`). I placed some code at the start of the `keypress` handler that checks if the last character is a `zwsp` and removes it. – rphv Mar 04 '16 at 01:07
  • Note that this won't handle the case where you break an existing line of text - for that, you'll have to implement a check to see if you're starting a new line or not, possibly using the [strategy outlined here](http://stackoverflow.com/a/7473638/1612562). – rphv Mar 04 '16 at 01:14
  • 1
    @rphv you're brilliant you gave me an idea of doing something better to remove those characters :) I might post the code later when I finish it, just one more thing, if the caret is inside a span element and I hit enter, do you know how to close the span tag and add the br after it then open a new span tag ? That's the only piece I need, Thank u so much. – medBouzid Mar 06 '16 at 23:47
  • @medBo - I edited the code snippet above. You can check if the cursor (carat) is in a `` element like this: `document.getSelection().anchorNode.parentNode.nodeName == "SPAN"`. Then, move the carat out of the ``, insert the line break `
    ` tag, and create a new span for the next line.
    – rphv Mar 07 '16 at 18:03
1

You can see a full cross browser implementation here. There are so many hacks to make it work. This code from the link would help you to device a solution.

Example of a Geko and IE hack:

doc.createElement( 'br' ).insertAfter( startBlock );

// A text node is required by Gecko only to make the cursor blink.
if ( CKEDITOR.env.gecko )
    doc.createText( '' ).insertAfter( startBlock );

// IE has different behaviors regarding position.
range.setStartAt( startBlock.getNext(), 
    CKEDITOR.env.ie ? CKEDITOR.POSITION_BEFORE_START :
        CKEDITOR.POSITION_AFTER_START );
Sufian Saory
  • 862
  • 9
  • 14