32

I have observed an undesirable behaviour in Chrome that occurs when one joins two <p>'s by deleting the separation between them. Although the <p> tags are joined properly, Chrome wraps the right-most <p> tag's content with a <span>.

Edit: this happens for all block elements, not just p tags.

Example:

For example, when the separating </p><p> are deleted from the following block:

<div contenteditable="true"><p>p one.</p><p>p two.</p></div>

It becomes:

<div contenteditable="true"><p>p one.<span style="font-size: 16px; line-height: 1.44;">p two.</span></p>

Example in a fiddle: Chrome wrapping contents of joined <p> with a <span>.

Question:

Is there an easy way to prevent chrome from doing this? It results in horrible markup that I'd like very much to be rid of.

Petah
  • 45,477
  • 28
  • 157
  • 213
Michael Robinson
  • 29,278
  • 12
  • 104
  • 130
  • 1
    well i don't think you can really *prevent* chrome from doing it. i think you need to check if such things happens on a key event and fix manually. i spend much time the last months to customize ckeditor to my needs and looked much at its internals. and it is a huge bunch of code fixing various browser bugs like this. btw i could not reproduce that behavior in chrome '23.0.1271.64' – t.niese Feb 26 '13 at 08:39
  • @t.niese how would you reasnobally check for such an event? – Petah Feb 26 '13 at 09:36
  • @Petah assuming you have no inline styling of spans in your page - I suppose you could use javascript to remove spans with the style attr. (PS: I know that this is a big assumption) – Danield Feb 26 '13 at 09:44
  • @Danield yea, sorry that assumption is not feasible. – Petah Feb 26 '13 at 09:56
  • 1
    Practically, what is the real problem? Besides having a weird DOM tree.. I mean, what's the real-life use case.. – George Katsanos Feb 26 '13 at 10:35
  • 2
    @GeorgeKatsanos if you do much editing the DOM could get over populated with unnecessary elements very fast. and filtering them later server side could lead to other problems. for my situation it was that the generated html code become many times larger than it would have been required. – t.niese Feb 26 '13 at 10:41
  • 2
    @GeorgeKatsanos we develop an open source WYSIWYG editor, and there is no guarantee that inserting element like this wont break end users websites, or the functionality of the editor. Plus no one like messy source code. – Petah Feb 26 '13 at 19:54
  • There's an answer yesterday that I think explains why Chrome does this, but it's gone now, so I made an example to show: http://jsfiddle.net/THPmr/5/ With a simple [Delete] and [Enter], Chrome maintains the style, while Firefox "pollute"s the next paragraph. – Passerby Feb 27 '13 at 04:32
  • I reported these issues a long time ago (http://code.google.com/p/chromium/issues/detail?id=226941 and https://bugs.webkit.org/show_bug.cgi?id=114791), because many CKEditor users [reported it to us](http://dev.ckeditor.com/ticket/9998). Unfortunately, no response in half of a year... – Reinmar Sep 09 '13 at 07:02

9 Answers9

11

There is a way but you need to pro-actively set a few styles. The idea is to tell Chrome that the styles are already taken care of, so it doesn't need to add SPAN to meet the styles requirement. basically, you need to add the chrome added styles to a span class under your contenteditable div ( see example below).

Edited fiddle

For you example:

  • I added an "edit" class to the contenteditable DIV
  • I added an .edit p, span class in the style

This becomes:

.edit {
  border: 1px solid gray;
  padding: 10px;
}
.edit p, span {
  line-height: 1.44; font-size: 16px;
}

And the DIV:

<div contenteditable="true" class="edit">...</div>

Note that you normally don't need the font-size: 16px;. I needed to add this one because fiddle defines some font size in contenteditable. On a standalone page I didn't need it.

You need to apply this Chrome 'patch' to any elements where it happens (so if you need UL, OL... then add what is needed following my example logic above)

JScoobyCed
  • 10,203
  • 6
  • 34
  • 58
  • This is a good answer, its too bad there is not enough time to investigate in order to award the bounty. – Petah Mar 05 '13 at 02:22
  • No worry. I've spent time working on contenteditable too and didn't met that issue (didn't realise, and doesn't affect my needs :) ) – JScoobyCed Mar 05 '13 at 02:26
  • 3
    Yeah, this works nicely IFF all content in your contenteditable div has the same `font-size`, `line-height` and `color`. As soon you have various types of elements with various font sizes/colors within one and the same contenteditable, you are basically f#cked. The span that is created by Chrome holds styling that corresponds to the styling of the element that the text inside the span _originated from_ (as opposed to the element that the text _is currently in_). This means that setting styling in advance is impossible, as you no longer know which will have the intended preemptive effect. – Tim Molendijk Feb 03 '14 at 18:51
6

I know it is not really an answer to solve it, but a hint how it could be fixed (but it is to long to be a comment to Petah question how i would solve it)

in general you would check when such bugs could happen. for the case of the span creation you would listen to all keydown and keypress events and check if the key is the backspace/delete key or for every key that inserts chars if it is a real selection.

if this is the case then you need to check the current selection (either the position of the insert mark, or the real selection) then you know which is the next following text-element or node. then you need to check the in the next following keypress and keyup if there is a span created directly after your insert mark. depending on the browser bug you need some further checking. if there is one create unwrap its content again. additionale Mutation events and helper attributes could be used.

But i need to say that i gave up in doing this myself and switched over to ckeditor 4. most of the it's features i don't need and it is a really big library. but cause of the huge number of such bugs i did not see another solution for me.

EDIT Here an update of the js fiddle that shows the idea with a Mutable event: http://jsfiddle.net/THPmr/6/

But that is not bullet proofed, it is just to show how it could be achived ( only tested in chrome 27.0.1422.0, and probably would not work if more then one text element is contained in the second p )

t.niese
  • 39,256
  • 9
  • 74
  • 101
  • Unfortionalty its not really that simple as its not only the delete/backspace that could cause this. You can also make a selection like this `...{ph.

    Th}...` and press and key (like space or a letter) and it results in the same effect. Sure I could check every key press then try and detect this, but that is very complicated and costly perfomace wise.

    – Petah Feb 26 '13 at 09:59
  • @Petah yes sorry ... just realized that .. if it is a real selection you need to treat it as a delete. but it is the same technique. – t.niese Feb 26 '13 at 10:00
  • @Petah you are right about the *perfomace* but this is the way these different editors are working around this bugs. you need to spend much time to detect the various bugs, and to find a way how to solve them in a effective way. – t.niese Feb 26 '13 at 10:06
  • @Petah i added a sample using mutable event, i'm pretty sure it won't work in all cases, but probably it is a base to work on. – t.niese Feb 26 '13 at 10:38
  • It doesn't work for me, placing cursor at terminus of top`p` and pressing `delete` results in the same output. – Michael Robinson Feb 26 '13 at 22:08
  • @MichaelRobinson as noted it is more a concept then a solution (i only implemented the `backspace` but now updated to also use `delete`. **but** a more reliable way would be to check on these event - where element like `span` was added - if the style attributes of this element differ from the parent of the `span` and if not then unwrap the content of it, so that e.g. style differences would be preserved - for the case where this is a requirement, but this really depends on the what you want to achieve) – t.niese Feb 27 '13 at 14:09
  • Created a version that uses Mutation observer instead of mutation events (that seem to be deprecated). Has the same limitations as the original, though: https://jsfiddle.net/z4hz6vxd/ – Borgtex Aug 22 '16 at 15:49
4

Here is my take on removing the extra spans

document.querySelector('[contenteditable=true]')
  .addEventListener('DOMNodeInserted', function(event) {
    if (event.target.tagName == 'SPAN') {
      event.target.outerHTML = event.target.innerHTML;
    }
  });
select
  • 2,513
  • 2
  • 25
  • 36
3

The CSS is influencing how the markup is made inside contenteditable:

div, pre { border: 1px solid gray; padding: 10px; line-height: 1.44; }

Delete the line-height line and the problem doesn't occur any more.

There are in general several bugs with contenteditable related to default styling : How to avoid WebKit contentEditable copy-paste resulting in unwanted CSS?

EDIT JsFiddle IS indirectly influencing this (tinkerbin behaves differently) because of its' CSS (normalize.css). Try this:

  1. Run your fiddle
  2. Inspect a <p>
  3. Disable all font-size declarations in the CSS stack - including your line-height
  4. do the backspace
  5. there is no inline span

Solution 1 : Use classes and id's.

Don't declare font-size for p or div but for p.main-content, or more simply, .main-content. If the font-size of your elements inside contenteditable is coming from the browsers' internal default CSS then Chrome won't add extra markup/inline styling.

Solution 2 : Use a Sanitizer.

I'd personally go with #1 as it's never a good practice to specify font-sizes and typo in so generic tags.

Community
  • 1
  • 1
George Katsanos
  • 13,524
  • 16
  • 62
  • 98
3

The best way I found so far is to listen to DOMNodeInserted and check the tagName. If it is a span, you can remove the tag and but leave the contents. This also keeps the cursor at the correct place.

function unwrap(el, target) {
    if ( !target ) {
        target = el.parentNode;
    }
    while (el.firstChild) {
        target.appendChild(el.firstChild);
    }
    el.parentNode.removeChild(el);
}

var AutoFix = true;

document.getElementById('editable')
.addEventListener('DOMNodeInserted', function(ev) {
    if ( !AutoFix ) {
        return;
    }
    if ( ev.target.tagName=='SPAN' ) {
        unwrap(ev.target);
    }
});

I've added a boolean 'AutoFix' so you can disable the automatic dom changes when you do need to insert a span, since this event fires on any dom change. E.g. if you have a toolbar that allows the user to insert something like <span class="highlight">...</span>.

The code has no side effects in IE or FireFox as far as I can see.

Auke
  • 116
  • 3
1

This irritated me as well, but I found a solution that works well enough for now.

$('#container span').filter(function() {
    $(this).removeAttr("style");
    return $(this).html().trim().length == 0;
}).remove();

I simply remove the style tag from the span element and remove it altogether if it's empty. You could probably filter based on the attribute style, but as I'm already doing a loop to check to remove empty spans, I thought it was best to do both at the same time.

It does create a flicker for a microsecond when chrome first tries to insert the style inherited for the span, but you only see that once immediately after deletion.

It isn't perfect, but it's quick and concise.

Regular Jo
  • 5,190
  • 3
  • 25
  • 47
0

Found this link from a comment posted on someone's blog:

(Fixed bug where the latest WebKit versions would produce span element…) https://github.com/tinymce/tinymce/commit/8e6422aefa9b6cc526a218559eaf036f1d2868cf

Jags
  • 1,466
  • 1
  • 18
  • 33
0

Please see the answers here: https://stackoverflow.com/a/24494280/2615633 To fix the problem you may just use this plugin: jquery.chromeinsertfix

Community
  • 1
  • 1
UndeadBane
  • 443
  • 5
  • 7
0

After several attempts of using provided solutions, I came up with this script:

var content = obj.textContent;
obj.innerHTML = '';
obj.textContent = content;

When someone pastes a text with html encoded chars, putting it to innerHTML results with a valid html tags, that is why I decided to purge innerHTML content before placing content into obj

qba-dev
  • 166
  • 1
  • 8