Working around Chrome's contenteditable span bug



Recently, we've been experiencing an issue with span insertion in our WYSIWYG editor. I want to bring it to light in case other developers are experiencing the same, frustrating issue.

The issue turned out to be specifically with Google Chrome, which we love and use almost exclusively internally and for development. It's fast and reliable, but as you'll see, it's not without quirky bugs.

The bug

The issue is that in a contenteditable area, when merging two elements together (like, but not limited to, paragraphs and headings), a span with inline styles will be inserted around the contents of what was the second element (demo of the issue).

For example, let's say you've got 2 paragraphs:

<body contenteditable="true">
  <p>Paragraph content ONE</p>
  <p>Paragraph content TWO</p>
</body>

Now you backspace from the beginning of the second paragraph, combining the two into one. This is what you would expect:

<body contenteditable="true">
  <p>
    Paragraph content ONE Paragraph content TWO
  </p>
</body>

But in Chrome this is what happens:

<body contenteditable="true">
  <p>
    Paragraph content ONE <span style="line-height: 1.5em">Paragraph content TWO</span>
  </p>
</body>

You get a span with an inline style! WHAT TRICKERY IS THIS? In fact, if you have other styles defined on span it will take all rules and insert them as an inline style. In our case, we were seeing font-family, font-size, and line-height, among others.

Shortly after realizing this was probably related to Chrome I found this Stackoverflow question that described the issue exactly.

The issue is especially bad when using contenteditable without an iframe since your editor styles aren't sandboxed from from the rest of the document. I was testing this with Redactor and CKEditor which both use contenteditable inside of an iframe. The problem is alleviated somewhat like this, however, it does not prevent the span insertion.

In the same Stackoverflow question a basic solution was proposed using the DOM Mutation event to remove the span. It's not a perfect solution and it won't work perfectly in all cases, but this is ultimately the solution that I went with.

The patch

I added a method to Redactor to check for the span.

checkForSpan: function() {
  this.$editor.on("DOMNodeInserted", $.proxy(function(e) {
    if (e.target.tagName == "SPAN" )Â {
      var helper = $("<b>helper</b>");

      $(e.target).before(helper);

      helper.after($(e.target).contents());
      helper.remove();

      $(e.target).remove();
    }

  }, this));
}

This bug was crippling for the content editor. Fixing this majorly helped with reliability during editing, making our lives and our clients lives less stressful.