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

Disable inline filling character removal during composition #1355

Closed
wants to merge 23 commits into from
Closed

Conversation

f1ames
Copy link
Contributor

@f1ames f1ames commented Mar 13, 2018

Suggested merge commit message (convention)

Fix: Prevent inline filler removal during composition. Closes ckeditor/ckeditor5#4033. Closes ckeditor/ckeditor5#4340.


Additional information

There is still one issue (ckeditor/ckeditor5#4307) which is not resolved by this fix due to native Chrome bug.

This PR adjusted the way how inlineFiller is handled in non-empty text nodes. Previously there was an assumption that in nodes like FILLERtext{}, the filler should not be touched. But there were no real cases producing such situations.
Blocking inline filler removal during composition is now such case (and the only we know ATM) so it seems reasonable to just adjust this behaviour, so the filler gets removed also in such situations.

@coveralls
Copy link

coveralls commented Mar 13, 2018

Coverage Status

Coverage remained the same at 100.0% when pulling aa32fff on t/898 into 868d79b on master.

@Reinmar
Copy link
Member

Reinmar commented Mar 23, 2018

@Mgsy could you test whether this PR doesn't introduce some regressions? Feel free to talk with @f1ames to learn what might got changed.

@Reinmar Reinmar requested review from Reinmar and Mgsy March 23, 2018 09:18
@f1ames
Copy link
Contributor Author

f1ames commented Mar 23, 2018

Just to describe in more details changes in this PR so it may be helpful during testing / looking for regressions.

There are two main changes in the renderer here:

Preventing inline filler removal during composition

Every time composition is started inside element which has inline filler (so basically empty inline element), inline filler will not be removed until composition is finished. Before this change the inline filler was simply removed every time first character was inserted inside such elements. Now if character is inserted by composition, the filler will not be removed. That means the modified behaviour is only observable during composition.

The one problem I could think of is that renderer is still not perfect when it comes to handling composition and it tent to break it in some cases (especially when composing inside empty elements). Sometimes in such cases the compositionend event will not be fired and inline filler may not be removed properly (but it will be removed later when composition is properly ended).
This is more general problem and it needs other improvements (so renderer will not break composition).

Adjusting the general behaviour when filler is removed

It is already mentioned in the PR description:

This PR adjusted the way how inlineFiller is handled in non-empty text nodes. Previously there was an assumption that in nodes like FILLERtext{}, the filler should not be touched. But there were no real cases producing such situations.

When renderer encounters already rendered text node with content like FILLERtext{}, it automatically removes (in general, not only during composition) inline filler. This was basically needed for properly handling filler removal after composition.

The previous behaviour assumed that filler in such cases should not be touched because it was left there on purpose. However, such situations will not have place as the filler is removed right after something is inserted in the empty node with filler. So if there is somewhere any case like above in which inline filler should not be touched in a content like FILLERtext{} (apart from composition ofc), the regression could occur there.

@Mgsy
Copy link
Member

Mgsy commented Mar 26, 2018

I've noticed one regression. Putting the selection in a link inserts the inline filler. The caret becomes invisible, the link is divided and after typing some text, more <a> elements appear.

After removing the link, <a> element with the inline filler is still in the DOM.

GIF - Click.

It occurs in all browsers.

@Mgsy
Copy link
Member

Mgsy commented Mar 26, 2018

Chrome

Steps to reproduce

  1. Open the article sample.
  2. Put the caret in Paragraph.
  3. Start the composition.
  4. Press Shift + Arrow up.

Current result

The inline filler has been inserted to the Heading 1. It doesn't disappear after finishing the composition.

GIF

Click.

@@ -104,6 +104,13 @@ export default class Renderer {
*/
this.isFocused = false;

/**
* Indicates if composition takes places inside view document.
Copy link
Member

Choose a reason for hiding this comment

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

"Indicates whether text composition takes place in the document."

(you can also update isFocused description)

@@ -182,7 +189,8 @@ export default class Renderer {
// There was inline filler rendered in the DOM but it's not
// at the selection position any more, so we can remove it
// (cause even if it's needed, it must be placed in another location).
if ( this._inlineFiller && !this._isSelectionInInlineFiller() ) {
// Filler should not be touched during composition to not break it.
if ( this._inlineFiller && !this.isComposing && !this._isSelectionInInlineFillerOnlyNode() ) {
Copy link
Member

Choose a reason for hiding this comment

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

Could you explain the change from isSelectionInInlineFiller() to isSelectionInInlineFillerOnlyNode()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Described it in more detail in comment above - #1355 (comment) in Adjusting the general behaviour when filler is removed section.

@@ -646,6 +646,80 @@ describe( 'Renderer', () => {
expect( domSelection.getRangeAt( 0 ).collapsed ).to.be.true;
} );

it( 'should add and remove inline filler in case <p>foo<b>[]bar</b></p>', () => {
Copy link
Member

Choose a reason for hiding this comment

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

I feel that these two tests are too long. Can't they be split into more concrete parts?

Copy link
Member

Choose a reason for hiding this comment

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

E.g. there's just one new test for the whole change with not removing the filler during composition while there are at least two things to check – whether the filler is not touched during composition and that it's removed when it can be removed.

I mean – I know there are some pretty long tests in this file already, but your tests are twice as long :D

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The test is super long, I think I tried to omit some initialization code repetition and merged two checks into one test... I will split it and introduce more test to cover other cases if needed.

@Reinmar
Copy link
Member

Reinmar commented Mar 26, 2018

After some bold/italic buttons clicking, typing and moving the caret around (by clicking) I got two fillers:

image

And once there are two+ fillers, only one of them is skipped on left/right arrow keys. The other require 7 key presses.

@Reinmar
Copy link
Member

Reinmar commented Mar 26, 2018

Another interesting observation – sometimes, selecting text from right to left causes adding the filler to every text node in that selection :D But I can't repeat it in a stable manner.

Also, to be clear, multiple fillers is not something we certainly need to avoid. However, I wonder whether we shouldn't remove the previous filler if the selection needs a new one. So, to never allow to have two fillers at the same time. WDYT, @f1ames? Could we do that?

@Reinmar
Copy link
Member

Reinmar commented Mar 26, 2018

BTW, should I notice any composition case getting fixed? Or are all of them still broken because we're re-rendering too big part of the tree?

@f1ames
Copy link
Contributor Author

f1ames commented Mar 29, 2018

BTW, should I notice any composition case getting fixed? Or are all of them still broken because we're re-rendering too big part of the tree?

The second one, so there are not visible improvements due to how rerendering works now.

@f1ames
Copy link
Contributor Author

f1ames commented Mar 29, 2018

I've noticed one regression. Putting the selection in a link inserts the inline filler. The caret becomes invisible, the link is divided and after typing some text, more elements appear.

@Mgsy for some reason I am not able to reproduce this issue. Are there any specific steps needed?


What I noticed looking at the provided gif is that in you case "link selection span" is inside a element.

image

When I run similar test locally I see that span is outside a 🤔

image


Putting the selection in a link inserts the inline filler.

Also I don't see inline filler on the provided gif, only span marking link selection. Inline filler is shown in a browser dev tools like

image

Checked on http://localhost:8125/ckeditor5-core/tests/manual/article.html with Chrome (Version 65.0.3325.162).

@f1ames
Copy link
Contributor Author

f1ames commented Mar 29, 2018

I was able to reproduce the 2nd issue (only on windows as on macOS shift + up behaves a little different). The main issue here is that shift + up doesn't cause compositionend. I will look for a workaround for this.

@f1ames
Copy link
Contributor Author

f1ames commented Mar 29, 2018

So, to never allow to have two fillers at the same time. WDYT, @f1ames? Could we do that?

I think that would be a valid approach.

@Mgsy
Copy link
Member

Mgsy commented Apr 3, 2018

@Mgsy for some reason I am not able to reproduce this issue. Are there any specific steps needed?

Fortunately, after pulling latest changes I can't reproduce it too 🎉

@f1ames
Copy link
Contributor Author

f1ames commented Apr 11, 2018

I redesigned this solution a bit, so now it handles more cases. In the ideal world, handling inline filler should work like:

  1. Inline filler is inserted in the empty inline element (currently selected).
    • When something is typed, inline filler is removed.
    • Or when composition starts, inline filler is not removed. After composition ends (by changing selection or inserting composed characters), filler is removed.

However, there are other cases:


The proposed solution handles all above situations:

  • For regular typing, inline filler is removed as it was before.
  • During composition in empty inline element, filler is not removed. It is removed after composition ends.
  • If selection is extended during composition, inline filler would not be touched (because you can go back to composing). If selection would be moved (thus ending composition) filler is removed too.
  • If there is a case that for some reason (e.g. broken composition), selection is moved to a different node during composition (different from the one in which composition started) without firing compositionend, the filler will also be removed.

The only issue here is that if composition is not ended properly (last point above), regular typing in empty inline element will not remove filler immediately (because renderer still thinks composition takes place). The filler will be removed after selection changes or composition ends. So it is still guaranteed that there will be only one inline filler and that it is removed right after selection changes or composition ends. This is an edge case (now it is caused by rerendering element nodes in which composition takes place) and such handling should be sufficient so nothing gets broken and inline fillers are still properly handled.

@f1ames f1ames requested a review from Reinmar April 11, 2018 12:43
@Mgsy
Copy link
Member

Mgsy commented Apr 16, 2018

I've found another case.

Steps to reproduce

  1. Open the article sample.
  2. Put the caret at the end of the Paragraph.
  3. Press Ctrl + B.
  4. Start the composition and type something.
  5. Press Ctrl + B.
  6. Press Shift + Arrow up.

Current result

  • After step 5, the inliner filler is moved outside of the <strong> element
  • After step 6, the inline filler appears at the beginning of the block

GIF

bug_letters

@f1ames
Copy link
Contributor Author

f1ames commented Apr 18, 2018

@Mgsy I see it's Windows only (e.g. on macOS you cannot change styling during composition). As for step 6., it behaves as described but with Shift + Arrow up for me.

Step 5. is correct (well, almost - #1409) and behaves the same without composition. While you are on end of paragraph and after strong element, filler is needed there (tested with beta.2 on https://ckeditor5.github.io/):

image

Step 6. is incorrect as filler should be removed in such cases.


The thing is, we assumed that we shouldn't remove inline filler during composition and try to not rerender any part of the text node in which composition takes place. Here when switching off bold we need to render it which breaks composition. I think it will be good to report as separate issue (that you can change styling during composition which breaks it - extracted to ckeditor/ckeditor5#976). Still, I will take a look on reported issue with fillers management if it can be improved.

@Mgsy
Copy link
Member

Mgsy commented Apr 18, 2018

@Mgsy I see it's Windows only (e.g. on macOS you cannot change styling during composition). As for step 6., it behaves as described but with Shift + Arrow up for me.

You're right, it can't be reproduced on macOS. And of course in the last step I meant Shift + Arrow up :) I'll edit the scenario.

@f1ames
Copy link
Contributor Author

f1ames commented May 8, 2018

I improved inline filler handling during composition for non-collapsed selection (fixing #1355 (comment)).

The complexity of this PR (the whole solution basically) is mainly caused by some composition edge cases (as mentioned in #1355 (comment)). So apart from simple cases (composition starts and ends in the same element), it handles extending selection during composition (cases mentioned by @Mgsy) and also deals with not properly ended composition (when compositionend is not properly fired).

Broken composition may occur when element in which composition takes place is rerendered, but also during collaborative editing. So even if we are planning to fix as many cases as possible it still may happen that composition will break at some point and renderer should be able to handle such situations without spoiling editing experience.

@f1ames
Copy link
Contributor Author

f1ames commented May 9, 2018

Ready for re-review, cc @Mgsy @Reinmar.

@Mgsy when testing Shift + Arrow up please keep #1416 in mind.

Copy link
Member

@Mgsy Mgsy left a comment

Choose a reason for hiding this comment

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

I've tested it after the latest changes and everything seems to work fine 👌

} else if ( this.isComposing ) {
// When selection has 0 ranges, `isCollapsed` returns false, but here
// we are only interested in non-collapsed selection (so with at least 1 range).
if ( !this.selection.isCollapsed && this.selection.rangeCount > 0 ) {
Copy link
Member

@Reinmar Reinmar May 23, 2018

Choose a reason for hiding this comment

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

In such cases, to make the code more readable is good to split the code into two steps:

// When selection has 0 ranges, `isCollapsed` returns false, but here
// we are only interested in non-collapsed selection (so with at least 1 range).
const isSelectionNotCollapsed = !this.selection.isCollapsed && this.selection.rangeCount > 0;

// Explain what we do if it's not collapsed and someone's composing.
if ( isSelectionNotCollapsed ) {

Now, for an explanation how we handle this case you need to dive into the body of this if() while it's a bit too late. When scanning a code you should not need to go that deep.

// node (using 'shift + up' in Chrome on Windows during composition). In such situations
// filler should not be moved or deleted (because it is possible to continue composing).
inlineFillerPosition = this._getExistingInlineFillerPosition();
} else if ( !this._isValidCompositionSelection() ) {
Copy link
Member

Choose a reason for hiding this comment

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

This function is used only in this place so it could be made more contextual. Like _checkCompositionWasNotEndedCorrectly().

@Reinmar
Copy link
Member

Reinmar commented May 23, 2018

We've just talked with @f1ames that this PR doesn't actually change the behaviour much when typing (the filler is maintained if the mapping worked) because of this:

// Possible options are:
// "FILLER{}"
// "FILLERadded-text{}"
const selectionPosition = this.selection.getFirstPosition();
const position = this.domConverter.viewPositionToDom( selectionPosition );

It's even reported in https://github.com/ckeditor/ckeditor5-engine/issues/1409.

This PR will make filler stay longer if the composition takes place. But if the selection left the text node in which the filler was, this PR will also make sure that the filler is removed (because we might've encountered an incorrectly terminated composition). So, to sum up, the only thing which this PR changes is the fact that the filler will be kept if the composition takes place when the selection is non-empty (on master it's enough that the selection is not empty for the filler to be removed).

Therefore, we need to reconsider whether merging this PR makes sense. Perhaps it's enough to keep the current behaviour (keeping the filler as long as the selection stays in the same text node) + extending this behaviour to non empty selection + extending jumpOverInlineFiller() for these scenarios + fixing mapping early so isInlineFillerInSelection() can rely on that.

@f1ames
Copy link
Contributor Author

f1ames commented May 23, 2018

See also https://github.com/ckeditor/ckeditor5-engine/issues/1342#issuecomment-391422791 which extends the above comment.

@Reinmar
Copy link
Member

Reinmar commented Nov 7, 2018

It turned out that this change isn't needed because the renderer was designed to not remove the inline filler while the selection is in the same text node anyway. That was buggy but since we fixed it in #1424 it works fine. There's not need for any additional blocking. At least we think so now...

@Reinmar Reinmar closed this Nov 7, 2018
@Reinmar
Copy link
Member

Reinmar commented Nov 7, 2018

Please do not remove the branch.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
4 participants