Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Mention post-fixer now correctly handles undo. #17

Merged
merged 25 commits into from
Apr 2, 2019
Merged

Mention post-fixer now correctly handles undo. #17

merged 25 commits into from
Apr 2, 2019

Conversation

jodator
Copy link
Contributor

@jodator jodator commented Mar 28, 2019

Fix: Mention post-fixer now correctly handles partial mentions on insert, remove and paste. Closes ckeditor/ckeditor5#4629. Closes ckeditor/ckeditor5#4622.

@jodator jodator requested a review from Reinmar March 28, 2019 13:16
@scofalik scofalik requested review from scofalik and removed request for Reinmar March 28, 2019 17:28
@scofalik
Copy link
Contributor

scofalik commented Mar 29, 2019

Three things:

  1. In insertions, you need to check deeply for inserted text nodes. When you inserted a paragraph, a partial mention might be in that paragraph. Differ returns only the top-level changes (it is optimized for downcasting). You may write it as a util (or a method on Differ) because it is not the first time we are having trouble with this. Or we could have some kind of flag like .getChanges( { deep: true } );.
  2. Shift+enter and also pasting does not work correctly.
  3. If we agree that mentions are always on exactly one text node (we don't allow partial styling) I am not sure if we need the "additional check for deleting last character". When is it needed? If you delete the last character in mention then, well, first of all, it would have to be a one-character-mention :D second of all, you don't need to remove the attribute from it if the whole mention is gone. So I think it is not needed?

@jodator
Copy link
Contributor Author

jodator commented Mar 29, 2019

  1. If we agree that mentions are always on exactly one text node (we don't allow partial styling) I am not sure if we need the "additional check for deleting last character". When is it needed? If you delete the last character in mention then, well, first of all, it would have to be a one-character-mention :D second of all, you don't need to remove the attribute from it if the whole mention is gone. So I think it is not needed?

AFAIR it's an edge-case when selection is after mention and you remove last character (ie d in @Ted) then the previous check wouldn't work as selection is not inside text node.

@scofalik
Copy link
Contributor

Oh, ok, you meant "last" as "right-most" not "the only" :D. Yeah, I checked it and you are right.

@scofalik
Copy link
Contributor

So, in this case, we also need a check for the "left-most" character and I'd like to see a more clear comment. Use "left-most" and "right-most" or "at the beginning" and "at the end" instead of "first" and "last" to make it non-ambiguous.

Also, considering the other changes, I think that maybe structuring the post-fixer this way will be better:

for ( ... ) {
    if ( change.type == 'insert' ) {
        // Logic for insertions of text and non-text inside the mention.
        // And also looking for partial mentions in inserted content.
    } else if ( change.type == 'remove' ) {
        // Logic for removing of text inside, at the beginning and at the end of a mention.
    }
}

@jodator
Copy link
Contributor Author

jodator commented Mar 31, 2019

@scofalik: Ready to re-review. Two follow ups issues has been fixed:

  1. MentionUI doesn't have 100% CC #20
  2. You can type 1 char with mention (after changing selection attributes - ie enabling typing with bold). #21

Copy link
Contributor

@scofalik scofalik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some changes required :P

Other than that I miss two things in tests:

  1. In attribute post-fixer, I'd like to see if unbolding also works correctly (you can add it to the existing unit tests).
  2. I am missing clipboard test (pasting something like <blockquote><p>Foo bar</p><p>@jod</p></blockquote>.

src/mentionediting.js Outdated Show resolved Hide resolved
src/mentionediting.js Outdated Show resolved Hide resolved
src/mentionediting.js Outdated Show resolved Hide resolved
}

// Check text nodes on inserted elements (might occur when splitting paragraph on enter key).
if ( change.name != '$text' && change.type == 'insert' && schema.checkChild( change.name, '$text' ) ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

schema.checkChild() looks like a premature optimization. Is it needed? Does not having it produce any bugs?

Also, I think this might be incorrect, for example what if following is pasted:

<blockquote><p>@jod</p><p>Foo</p></blockquote>

blockQuote cannot contain text but there is still text node somewhere inside.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But this is fixed by upcast converter. This check is for splitting parent element with mention when split occurred inside text with mention. As such there is no need to check other cases then the elements that allow text inside.

src/mentionediting.js Outdated Show resolved Hide resolved
if ( change.type == 'remove' ) {
const nodeBefore = change.position.nodeBefore;
// Inserted inline elements might break mention.
if ( change.type == 'insert' && schema.isInline( change.name ) ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After resolving the above comment this might be the last usage of schema in this postfixer. If this is for optimization purposes, I'd consider removing this and removing the need of passing schema here. Keep it simple.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checkAttributeMentionOnNode already checks if the node is text so it will be safe.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no it explicitly checks for inserted in-line elements like <softBreak> which may break text (with mention.

src/mentionediting.js Outdated Show resolved Hide resolved
if ( change.name == '$text' && textNode && textNode.hasAttribute( 'mention' ) ) {
writer.removeAttribute( 'mention', textNode );
wasChanged = true;
for ( const child of insertedNode.getChildren() ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, if a deep structure was pasted, you need to go deeper.

for ( const item of writer.createRangeIn( insertedNode ) ) { ... }

// Yield text nodes with broken mention from the range.
for ( const textProxy of range.getItems() ) {
if ( !checkMentionAttributeOnNode( textProxy ) ) {
yield textProxy.textNode;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that this is covered by unit tests but I wonder out of curiosity when this happens?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, if that happens for mentions that are not on the boundary of the range. Some something like [foo<mention />bar].

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is used in attribute post-fixer:

fo[o <$text mention="{...}">men]tion</$text>

So when the range on which bold is applied is ends inside a mention. Then I want to get that dangling mention node on the right(left) side of range.end(start).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So isn't it that we are always interested in nodes next to range boundaries?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When applying attribute on this range yes.

tests/mentionediting.js Show resolved Hide resolved
@jodator
Copy link
Contributor Author

jodator commented Apr 2, 2019

@scofalik I've resolved all the issues from your comments.

I would leave those optimizations are they checks cases that might break mention during editing.

The pasting part of nested nodes is done by upcast converter - it does not allow partial mentions.

I've added tests for undo integration with attribute post-fixer.

@jodator jodator requested a review from scofalik April 2, 2019 12:03
@scofalik
Copy link
Contributor

scofalik commented Apr 2, 2019

The pasting part of nested nodes is done by upcast converter - it does not allow partial mentions.

Typically, I prefer having general solutions that cover the whole problem instead of directly solving the predicted cases. This is safer (and, in my opinion, correct) way to go. For example, I just had to fix a bug in view writer because some behavior was predicted only for one case while it could simply be inside a for loop following the special cases and applying wrapping/unwrapping to all children recursively.

This is why I have mixed feelings about it. I don't want to introduce redundant code that might never be used. But I also don't want to introduce potentially bad code. For now, if I didn't miss anything, the code is fine. However, if there is ever a feature (widget) that breaks blockquote, there will be a problem:

<blockQuote><paragraph><$text mention="jodator">@jod[]ator</$text></paragraph></blockQuote>

Paste the widget that can be only in the root at []:

<blockQuote><paragraph><$text mention="jodator">@jod</$text></paragraph></blockQuote>
<hypotethicalWidget />
<blockQuote><paragraph><$text mention="jodator">ator</$text></paragraph></blockQuote>

Or when a 3rd party plugin will simply split both paragraph and blockQuote. Or if we will support more nested structure and for some reason, we will break multiple blocks at once.

Anyway, it is hypothetical for now. I tried to think about cases in OT and block lists (the thing we plan to introduce, where you will be able to, for example, add multiple paragraphs, headers, maybe tables to list items) but I don't know if anything there will break this behavior.

So as I said, I have mixed feelings. I summon @Reinmar .

@jodator
Copy link
Contributor Author

jodator commented Apr 2, 2019

Paste the widget that can be only in the root at []:

OK valid TC. I've manged to do it with

blockquote > paragraph > mention

and split:

editor.model.change( writer => {
 writer.split( editor.model.document.selection.focus, editor.model.document.getRoot() );
} );

I'll update the PR for deep check.

@scofalik scofalik merged commit 07778e3 into master Apr 2, 2019
@scofalik scofalik deleted the t/12 branch April 2, 2019 16:17
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Tests fail Postfixer should remove the whole mention when it's build from two+ text nodes
2 participants