w3c / editing

Specs and explainers maintained by the editing task force
http://w3c.github.io/editing/
Other
193 stars 40 forks source link

Contenteditable re-creating deleted children when it shouldn't #468

Open johanneswilm opened 2 months ago

johanneswilm commented 2 months ago

In Chromium (possibly others), when deleting an inline element inside an contenteditable ement such as a <b> or a <span> and then typing a letter, the inline element gets recreated. This behavior is highly annoying for JavaScript editor creators. JavaScript devs have asked time and time for contenteditable to please stop trying to be "smart"and changing the dom in starneg ways.

A bug has been filed before [1], but it was marked as "won't fix" due to some wording in the (unreleased) execCommand document.

This issue is now also causing additional issues [2].

My proposal: Change the execCommand draft document removing any language that may make browser developers think that such behavior is wanted [3], Chromium should reopen the bug [1] and make Chromium be compliant with the Input Events spec.

[1] https://bugs.chromium.org/p/chromium/issues/detail?id=506858&no_tracker_redirect=1

[2] https://github.com/w3c/input-events/issues/158

[3] https://w3c.github.io/editing/docs/execCommand/#recording-and-restoring-overrides

saschanaz commented 1 month ago

Is this also something related? On Chrome, on contenteditable:

  1. Press Control+B
  2. Type foo
  3. <b> appears but formatBold doesn't fire, only insertText.
johanneswilm commented 1 month ago

@saschanaz Yes, that seems related. Except that what you describe is on initial input. That seems equally incorrect.

masayuki-nakano commented 1 month ago

Hmm, prohibiting it breaks the traditional behavior. Empty inline elements are not visible for users. Therefore, for avoiding unnecessary inline empty elements to stay, builtin editors need to clean it up as soon as possible. (E.g., if empty inline elements are stay, they need to be cleaned up when blur, etc.

Firefox supports document.execCommand("removeFormat") to clear the pending (cached) styles, but it seems that Chrome does not support (I'm not sure Safari, I cannot check it right now). It might be reasonable to add a new API to clear or disable caching inline styles. (document.execCommand("removeFormat") forcibly forgets current style at caret too.)

johanneswilm commented 1 month ago

From TPAC 2024 minutes:

#468

Demo: https://software.hixie.ch/utilities/js/live-dom-viewer/saved/13129 , https://software.hixie.ch/utilities/js/live-dom-viewer/saved/13130 Johannes: [explain the issue]

Anne: will it be a big UI change?

Olli: not for Firefox

Johannes: it’s not following the spec because it’s using an input so insert where it’s supposed to only insert text. But it’s inserting an entire element and the element comes out of nowhere

Anne: we can maybe change the event because it matches the platform behaviour. You should focus what event it should be instead

Johannes: ok. Maybe we should then have a special kind of

Anne: what happen when you do execCommand and then you start typing

Johannes: the thing is execCommand isn’t used, I mean, so I dont really know what’s happening, so we would need a different one

Anne: sorry I don't have enough context about this event. Perhaps we could have a format event? That might be a separate API that like typing start… reset the typing start to match the parent or something

Ryosuke: firing those events and adding a new type of event. A new API to get that typing information. Then editors have to get updated. I think this could defer to like an Edit or Maintainers. Case 1. You are using contentEditable, you want to retain the behavior that matches the platform; but if the App is, implementing their own code, they have their own text editing, editcontext,

Simon: in Firefox, the behaviour is different, depending on whether there is remaining text before the cursor

Megan: basically a style is a different tag, When you don’t have any text that element does exist

Anne: when the users starts typing, they migh expect it to be italic, but then the editor may prevent it. Once the element disappears from the tree, they also want to be able to reset the cursor, to be upright, when the user starts typing, they know it won’t be italic, and that requires a different solution, that’s why we might need both, or something else?

Olli: let’s say in Chrome, which adds this bold back, can you query that? Next time I’m typing here, will I get this extra bolder?

Ryosuke: query command should be done, state would return

@@

Simon: consider the idea of sending a format event when you start typing again, wouldn’t you also need to send a format event when you delete the last character? So that’s to send the states that the format is now gone

Johannes: when you delete the text, the delete, the last character which deletes the entire bold element, that’s something you have on that event, a target range

Anne: but that’s no format change

Johannes: no, so in deletion, if you did, it’s basically the event you get, the whole bold thing is being deleted, and insert and deletion work slightly differently with the input type, so if you delete an entire element, that includes formatting

Anne: when you get like the deleted content, it does remove the bold, but you dont get a format change event, so the format is still the same

Johannes: I understand the underlying model works differently

RN: one thing we could do in the browser is to leave the element in a contentable, one thing we could consider doing is just not delete in the target range, it will not include the bold element, next is to figure out is this a thing browsers can implement

Johannes: sounds good

Megan: +1 to the idea of keeping the bold and italic element

Simon: the cursor style depending on the actual DOM, not the internal editing state … we will need to send a delete event when you move the cursor

Johannes: the idea of with the contentEditable was that you can cancel all the before input event and do your DOM changes and you can rely on the browser not making its own dom changes that are not going through beforeinput events. …

RESOLUTION: Chromium/Webkit will check viability of leaving -element in place and sending delete event when caret moves.

masayuki-nakano commented 4 weeks ago

Well, I think that the solution is reasonable. However, I'm not sure whether it's reasonable to delete the empty inline elements at a caret move if there is no surrounding text. E.g., in an empty paragraph or around <img>. When I was implementing contenteditable=plaintext-only, I see browsers try to keep the style at inserting text as far as possible. I think that this is reasonable because web apps cannot change the text style with execCommand. So, once inline elements are lost, web apps cannot restore the style without modifying the DOM (i.e., they need to give up to use undo transactions of browsers). Therefore, I guess that it's better to keep inline elements when there is no surrounding text. For example:

<p><b>{}</b><img></p>
<p><img><b>{}</b></p>
<p><b>{}<br></b></p>

etc.

johanneswilm commented 1 day ago

@masayuki-nakano Fine with me. It just seems that means that if you create a pure contenteditable editor (using execCommand and browser undo-stack), your content will be filled with empty inline nodes that the user cannot easily remove. That seems a bit weird, but if we say that the better choice is always to use a JavaScript-based editor that has its own undo-stack, then sure, why not,