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

t/1301: The bindTwoStepCaretToAttribute should not fail in more complex cases #1406

Merged
merged 24 commits into from
Apr 18, 2018

Conversation

oleq
Copy link
Member

@oleq oleq commented Apr 16, 2018

Suggested merge commit message (convention)

Fix: The bindTwoStepCaretToAttribute behavioral helper should not fail in more complex cases. Closes ckeditor/ckeditor5#4277. Closes ckeditor/ckeditor5#4305. Closes ckeditor/ckeditor5#937. Closes ckeditor/ckeditor5#922. Closes ckeditor/ckeditor5#946.


Additional information

Note: A thorough testing required before merging. @Mgsy?

@oleq oleq requested review from scofalik and Mgsy April 16, 2018 12:20
@oleq
Copy link
Member Author

oleq commented Apr 16, 2018

I wonder if we should have a ticket test for every single issue mentioned here 🤔Wouldn't it be an overkill?

@coveralls
Copy link

coveralls commented Apr 16, 2018

Coverage Status

Coverage remained the same at 100.0% when pulling 4f6d9d9 on t/1301 into b54f26a on master.

@Mgsy
Copy link
Member

Mgsy commented Apr 16, 2018

Steps to reproduce

  1. Open the article sample.
  2. Create new link.
  3. Put the caret at the end of the link.
  4. Press Arrow right to leave the link.
  5. Activate bold and type one character.
  6. Press Arrow left few times.

Current result

The caret is stuck.

GIF

Click

It's a regression.

Edit: This scenario could be simplified. I've updated it.

@Mgsy
Copy link
Member

Mgsy commented Apr 17, 2018

Steps to reproduce

  1. Open the article sample.
  2. Put the caret at the end of the Paragraph word.
  3. Open the link balloon, type one character, i.e. x and add it.
  4. Put the caret at the end of the link.
  5. Press Arrow left.

Current result

The two-step caret movement doesn't activate and the selection is outside of the link.

GIF

Click

@Reinmar
Copy link
Member

Reinmar commented Apr 17, 2018

@scofalik, could you review this?

@Reinmar Reinmar requested review from scofalik and removed request for scofalik April 17, 2018 10:06
@scofalik
Copy link
Contributor

AFAIK @oleq is fighting with a nasty bug ATM.

//
// <paragraph><$text attribute>{}bar</$text>baz</paragraph>
//
if ( position.isAtStart && this._hasAttribute ) {
Copy link
Contributor

@scofalik scofalik Apr 17, 2018

Choose a reason for hiding this comment

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

I am not sure what exactly is this case here, but the comment makes me worried, that there's an error here.

Basically, Position#isAtStart will not return true if you are at the "beginning of an attribute". Or, more precisely, when a position is at the same offset when a Text node starts. So, in this example:

<paragraph>foo<$text linkHref="foo.html">{}link</$text>bar</paragraph>

Position#isAtStart returns false.

If you meant "at the beginning of an element", please fix the comment.

Copy link
Member Author

Choose a reason for hiding this comment

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

Bug in docs, OFC.

if ( isBetweenDifferentValues( position, attribute ) && this._hasAttribute ) {
this._preventCaretMovement( data );
this._removeSelectionAttribute();
} else {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this else needed here?


// DON'T ENGAGE 2-SCM if gravity is already overridden. It means that we just entered
//
// <paragraph>foo{}<$text attribute>bar</$text>baz</paragraph>
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this comment right? Shouldn't it be {}bar?

//
// <paragraph><$text attribute="1">foo</$text>{}<$text attribute="2">bar</$text></paragraph>
//
if ( isBetweenDifferentValues( position, attribute ) && this._hasAttribute ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't this condition be met in this case:

<paragraph><$text attribute="1">foo</$text><$text attribute="2">{}bar</$text>

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay I guess that in this scenario gravity is already overridden, so it would return earlier. If you think it is worthy to left a note about this, please do so. If not, it's fine.

@oleq
Copy link
Member Author

oleq commented Apr 17, 2018

Open the article sample.
Put the caret at the end of the Paragraph word.
Open the link balloon, type one character, i.e. x and add it.
Put the caret at the end of the link.
Press Arrow left.

It's not a bug. Setting selection using the mouse in a boundary situation (link is first/last in the block) will always put it inside the link. So no 2-SCM then. You have to use the arrow right keystroke to activate it.

const isAttrInNext = nextNode ? nextNode.hasAttribute( attribute ) : false;
const isAttrInPrev = prevNode ? prevNode.hasAttribute( attribute ) : false;

if ( isAttrInNext && isAttrInPrev && nextNode.getAttributeKeys( attribute ) !== prevNode.getAttribute( attribute ) ) {
if ( ( !isAttrInPrev && isAttrInNext ) || isBetweenDifferentValues( position, attribute ) ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not sure if I like isBetweenDifferentValues here, because it repeats some code from before. Maybe

isAttrInNext && ( !isAttrInPrev || prevNode.getAttribute( attribute ) != nextNode.getAttribute( attribute ) ) ?

const isAttrInNext = nextNode ? nextNode.hasAttribute( attribute ) : false;
const isAttrInPrev = prevNode ? prevNode.hasAttribute( attribute ) : false;

if ( ( isAttrInPrev && !isAttrInNext ) || isBetweenDifferentValues( position, attribute ) ) {
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 ( isBetweenDifferentValues( position, attribute ) && this._hasAttribute ) {
this._preventCaretMovement( data );
this._restoreGravity();
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder whether the selection should be restored in case. I guess there's a difference when this would be your model:

<paragraph><$text attribute="1" bold="true">foo</$text><$text attribute="2">{}bar</$text></paragraph>

Do we want the bold or not.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't say that current solution is incorrect, I just wonder.

Copy link
Member Author

Choose a reason for hiding this comment

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

In this case

<$text attribute="1" bold="true">foo</$text><$text attribute="2">{}bar</$text>

: the selection has bold="true" only (2-SCM active for attribute). It's a typing between two attribute boundaries of a different value case.

: the selection has attribute="1" bold="true"

If the selection lost bold="true" in the first step I'd personally find it confusing. Is that what you wanted to discuss?

Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if the selection should have bold after the first left-arrow press.

Copy link
Contributor

Choose a reason for hiding this comment

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

But this is nothing critical.

// If we are here we need to check if caret is a one character before the text with attribute bound
// `foo<a>bar</a>b{}iz` or `foo<a>b{}ar</a>biz`.
const nextPosition = position.getShiftedBy( -1 );
// DON'T ENGAGE 2-SCM if gravity is already overridden. It means that we have already entered
Copy link
Contributor

Choose a reason for hiding this comment

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

Outdated comment?

if ( this._hasAttribute ) {
this._removeSelectionAttribute();

return;
Copy link
Contributor

Choose a reason for hiding this comment

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

Unnecessary return

//
// <paragraph>{}<$text attribute>bar</$text></paragraph>
//
if ( position.isAtStart && isAtBoundary( position, attribute ) ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not sure if isAtBoundary is needed here if we check this._hasAttribute anyway.

Copy link
Contributor

@scofalik scofalik Apr 17, 2018

Choose a reason for hiding this comment

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

BTW. wouldn't using isAtStartBoundary be more precise and a tiny little bit faster? :trollface:

* @private
*/
_preventCaretMovement( data ) {
data.preventDefault();
Copy link
Contributor

Choose a reason for hiding this comment

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

Did you give it a thought to also cancel the event? I wonder if canceling the event wouldn't be correct. Of course, it works now as it is, so we don't have to change it, but IDK. We kind of cancel the event by handling it in quite a custom way so IDK. Both canceling and not canceling may create issues in future.

Copy link
Member

Choose a reason for hiding this comment

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

You can verify how important this is by checking how this works next to widgets which also try to handle left/right arrow keys ;)

Copy link
Member

Choose a reason for hiding this comment

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

PS. I think we should cancel the event and make sure that 2scm handles it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, you're right. The 2SCM doesn't work around widgets like image. But to get it working 2 things are neccessary:

  • evt.stop()
  • changing 2SCM keydown listener priority to highest because the Widget plugin already uses high :/

Copy link
Member

Choose a reason for hiding this comment

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

I love the second point. Can we use priorities.high + 1? Does on() allow specifying number still? I think it does because it should be using priorities.get().

Copy link
Contributor

Choose a reason for hiding this comment

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

Well, that's what priorities are for!

@scofalik
Copy link
Contributor

scofalik commented Apr 17, 2018

Review

I finished reviewing the code state as it is now. AFAIK @oleq is still fighting with some bugs but there is progress in this matter. After the bugs are fixed we could merge this PR.

This saying, in my opinion, this solution is too complicated at this point. I think there are two causes for that:

  1. @oleq inherited this code from @oskarwrobel and there was not much time to rethink the architecture of this solution.
  2. We had too few cases on our hands.

As much as I am for merging this PR (since it will make the whole feature more stable) I also think that we should discuss this feature once again, having the full view and discovering some cases that we didn't think about at the beginning.

Proposal

Personally, I think that we might be better dropping "override/restore" gravity as it brings a lot of complications. Dropping this solution and managing attributes directly through writer API may simplify this feature. Additionally, we will get rid of weird situations, where gravity overriding might do more than we'd like to, for example:

<$text linkHref="foo.html">foo{}</$text><$text bold="true">bar</$text>

If we override gravity to the ride, we start typing bold. Is it correct? Maybe - I am not saying it is not. But it is more mess and potential problems.

This feature seems like it shouldn't really be that difficult, though I guess that we made it too complicated by using wrong tool (gravity) to approach it.

Finally, even though we use selection gravity, we still use writer API to directly add or remove attributes from the selection anyway!

I have a feeling that there are just three things that should be taken into consideration:

  1. Does the selection has the attribute?
  2. Will the selection "leave the attribute"?
  3. Will the selection "enter the attribute"?

Which leads to those three scenarios:

  1. Has attribute + is leaving -> Remove the attribute, prevent caret move.
  2. Doesn't have attribute + is entering -> Add the attribute, prevent caret move.
  3. If approaching from the right side, remove the attribute when approaching the attribute (because the gravity is to the left). This, however, is tricky*.

How would it work for different cases?

Between attributes:

<$text attribute="1">fo{}o</$text><$text attribute="2">bar</$text>

-> not leaving or entering
<$text attribute="1">foo{}</$text><$text attribute="2">bar</$text>

-> has attribute + is leaving, remove attribute, prevent caret move
<$text attribute="1">foo</$text>{}<$text attribute="2">bar</$text>

-> does not have attribute + is entering, add attribute, prevent caret move
<$text attribute="1">foo</$text><$text attribute="2">{}bar</$text>

-> not leaving or entering
<$text attribute="1">foo</$text><$text attribute="2">b{}ar</$text>

<- not leaving or entering
<$text attribute="1">foo</$text><$text attribute="2">{}bar</$text>

<- has attribute + is leaving, remove attribute, prevent caret move
<$text attribute="1">foo</$text>{}<$text attribute="2">bar</$text>

<- does not have attribute + is entering, add attribute, prevent caret move
<$text attribute="1">foo{}</$text><$text attribute="2">bar</$text>

<- not leaving or entering
<$text attribute="1">fo{}o</$text><$text attribute="2">bar</$text>

Inside multiple attributes using 2SCM:
The trick is to handle each attribute separately and cancel handling if one of them was handled. So the keydown event should be stopped after 2SCM is used.

<$text a="1" b="2">fo{}o</$text>bar

-> not leaving or entering
<$text a="1" b="2">foo{}</$text>bar

-> has attribute a + is leaving, remove attribute, prevent caret move
<$text a="1" b="2">foo</$text><$text b="2">{}</$text>bar

-> has attribute b + is leaving, remove attribute, prevent caret move
<$text a="1" b="2">foo</$text>{}bar

-> not leaving or entering
<$text a="1" b="2">foo</$text>b{}ar

The beginning of an element:

<paragraph><$text a="1">f{}oo</$text></paragraph>

<- not leaving or entering
<paragraph><$text a="1">{}foo</$text></paragraph>

<- has attribute + is leaving, remove attribute, prevent caret move
<paragraph>{}<$text a="1">foo</$text></paragraph>

The end of an element:

<paragraph><$text a="1">fo{}o</$text></paragraph>

-> not leaving or entering
<paragraph><$text a="1">foo{}</$text></paragraph>

-> has attribute + is leaving, remove attribute, prevent caret move
<paragraph><$text a="1">foo</$text>{}</paragraph>

Approaching from the right side:

<paragraph><$text a="1">foo</$text>b{}ar</paragraph>

<- approaching from the right side, remove attribute a, remove attribute b
<paragraph><$text a="1" b="2">foo</$text>{}bar</paragraph>

<- does not have attribute a + is entering, add attribute, prevent caret move
<paragraph><$text a="1" b="2">foo</$text><$text a="1">{}</$text>bar</paragraph>

<- does not have attribute b + is entering, add attribute, prevent caret move
<paragraph><$text a="1" b="2">foo{}</$text>bar</paragraph>

<- not leaving or entering
<paragraph><$text a="1" b="2">fo{}o</$text>bar</paragraph>

Other questions regarding the solution

How to check "is leaving" and "is entering"?

This seems easy. We already check if the selection is at attribute boundary so we would just add checking if a proper key was pressed.

Clearing attributes set by 2SCM?

There's a question about clearing attributes set by 2SCM. However, maybe we don't need to do anything?

When the selection is moved through API (keypress, mouse click, directChange == true) then the attributes are cleared anyway. This means that we don't need to care about additional clearing.

* - However this messes up our solution when the caret approaches element from the right because the attribute would be removed first and then selection would clear that. So for that case, we would need more additional handling. I believe it is doable, though.

When the selection is moved because of changes to the model tree (changes in background, collaboration, directChange == false) we don't want those changes to affect selection anyway. The only exception would be if the selection is "removed" or the attribute is removed/changed. This should be doable too.

@scofalik
Copy link
Contributor

scofalik commented Apr 17, 2018

Now, as I am thinking about the solution I described, I got an idea. Maybe we could implement gravity managing as an additional separate feature. It would be much simpler. Basically, if a user clicks left arrow key, gravity is set to right, and when a user clicks right arrow key, gravity is set to right.

This would cover two things:

  1. We would not have the described problem in "approach from the right side" scenario.
  2. We (maybe) would give users a better UX for attributes for which we don't want a 2SCM.

How would it work?

fo{}o<$text bold="true">bold</$text>bar

-> keep gravity to the left
foo{}<$text bold="true">bold</$text>bar

-> keep gravity to the left
foo<$text bold="true">b{}old</$text>bar

<- keep gravity to the right
foo<$text bold="true">{}bold</$text>bar

<- keep gravity to the right
fo{}o<$text bold="true">bold</$text>bar

The gravity would be also reset on change:range with directChange set to true.

This is just an idea, I didn't think it through and I didn't think what problems it may bring. Also what problems it may bring in conjunction with 2SCM (although it would fix one issue :)).

return true;
}
const { nodeBefore, nodeAfter } = position;
const isAttrBefore = nodeAfter ? nodeAfter.hasAttribute( attribute ) : false;
Copy link
Contributor

Choose a reason for hiding this comment

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

I find those names confusing now. isAttrBefore checks nodeAfter. What does isAttrBefore mean? If anything, I'd suggest isBeforeAttr.

Copy link
Member Author

Choose a reason for hiding this comment

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

My bad. I changed it automatically and somehow make a mistake.

return true;
}
const { nodeBefore, nodeAfter } = position;
const isAttrBefore = nodeAfter ? nodeAfter.hasAttribute( attribute ) : false;
Copy link
Contributor

Choose a reason for hiding this comment

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

As above.

const isAttrInNext = nextNode ? nextNode.hasAttribute( attribute ) : false;
const isAttrInPrev = prevNode ? prevNode.hasAttribute( attribute ) : false;
const { nodeBefore, nodeAfter } = position;
const isAttrBefore = nodeAfter ? nodeAfter.hasAttribute( attribute ) : false;
Copy link
Contributor

Choose a reason for hiding this comment

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

As above.

@@ -380,6 +397,8 @@ class TwoStepCaretHandler {
if ( position.isAtStart ) {
if ( this._hasSelectionAttribute ) {
this._removeSelectionAttribute();
Copy link
Contributor

Choose a reason for hiding this comment

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

Why aren't we preventing caret move in this case? 🤔

Copy link
Member Author

Choose a reason for hiding this comment

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

Would that matter in this case? It's a start of the block anyway and we're moving left.

Copy link
Contributor

Choose a reason for hiding this comment

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

Doesn't it matter in case of <paragraph>foo</paragraph><paragraph><$text a="1">{}foo</$text></paragraph>? Won't we get to the previous paragraph if we won't stop caret movement?

Copy link
Contributor

Choose a reason for hiding this comment

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

I tested this in manual test and for some reason it works 🤷‍♂️

Copy link
Member Author

Choose a reason for hiding this comment

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

Hm, you're right. It should be prevented. But the funny thing is that it works. As long as the keydown event is stopped.

Copy link
Contributor

Choose a reason for hiding this comment

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

lol :)

apr-18-2018 16-01-40

@scofalik
Copy link
Contributor

Things I found in manual test (http://localhost:8125/ckeditor5-engine/tests/manual/two-step-caret.html):

  1. []<b><i>biz</i></b> after first right-arrow keypress becomes <b><i>[]biz</i></b> and then additional keypress does nothing.

  2. Minor thing, in one scenario a button on toolbar flickers:
    apr-18-2018 16-06-56

  3. This scenario from manual test fails, however I don't know how important it is, really:

Attributes set explicit
Press right arrow once again
selection should be at the same position
underline button should be not selected
(*) bold button should stay selected
  1. I don't like that there's no intermediate step between <u>bar</u><i>biz</i>.

  2. Filler bug is back:
    apr-18-2018 16-20-28

  3. You were supposed to merge http://localhost:8125/ckeditor5-engine/tests/manual/tickets/1301/1.html with http://localhost:8125/ckeditor5-engine/tests/manual/two-step-caret.html

  4. I miss an image in http://localhost:8125/ckeditor5-engine/tests/manual/two-step-caret.html test to see how 2SCM works with widgets.

… the boundary of an attribute should engage the two-step caret movement.
@oleq
Copy link
Member Author

oleq commented Apr 18, 2018

[]biz after first right-arrow keypress becomes []biz and then additional keypress does nothing.

I'm unable to reproduce it.

Minor thing, in one scenario a button on toolbar flickers:

It's because the engine sets attributes and we remove them :/

This scenario from manual test fails, however I don't know how important it is, really:

We sacrificed this when choosing _refreshAttributes() over _updateAttributes in LiveSelection.

I don't like that there's no intermediate step between barbiz.

Me too. But I'm afraid there's no easy way to do that. Different bindTwoStepCaretToAttribute "instances" would need to talk to each other to do that. ATM when we leave (arrow right) <u>bar[]</u><i>biz</i> we prevent the movement and override gravity (u attribute handler does that). To create that additional "empty" step we'd need to remove the selection attribute (i) too at the same time. And it's up to the i attribute handler. It would need to know what happened in the u handler to approach the situation properly.

@Reinmar
Copy link
Member

Reinmar commented Apr 18, 2018

apr-18-2018 17-27-43

@Reinmar
Copy link
Member

Reinmar commented Apr 18, 2018

OK, the above is not caused by 2scm.

…tribute-preceded-by-some-text-at-the-end-of-the-block-case.
@scofalik scofalik merged commit f0fd2d8 into master Apr 18, 2018
@scofalik scofalik deleted the t/1301 branch April 18, 2018 16:06
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
5 participants