-
Notifications
You must be signed in to change notification settings - Fork 40
t/1301: The bindTwoStepCaretToAttribute should not fail in more complex cases #1406
Conversation
…nrelated attributes inside the main attribute.
I wonder if we should have a ticket test for every single issue mentioned here 🤔Wouldn't it be an overkill? |
Steps to reproduce
Current resultThe caret is stuck. GIFIt's a regression. Edit: This scenario could be simplified. I've updated it. |
Steps to reproduce
Current resultThe two-step caret movement doesn't activate and the selection is outside of the link. GIF |
@scofalik, could you review this? |
AFAIK @oleq is fighting with a nasty bug ATM. |
// | ||
// <paragraph><$text attribute>{}bar</$text>baz</paragraph> | ||
// | ||
if ( position.isAtStart && this._hasAttribute ) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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 ) { |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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.
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 ) ) { |
There was a problem hiding this comment.
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 ) ) { |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 ) ) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
* @private | ||
*/ | ||
_preventCaretMovement( data ) { | ||
data.preventDefault(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 ;)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 tohighest
because theWidget
plugin already useshigh
:/
There was a problem hiding this comment.
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()
.
There was a problem hiding this comment.
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!
ReviewI 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:
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. ProposalPersonally, 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:
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:
Which leads to those three scenarios:
How would it work for different cases?Between attributes:
-> not leaving or entering -> has attribute + is leaving, remove attribute, prevent caret move -> does not have attribute + is entering, add attribute, prevent caret move -> not leaving or entering <- not leaving or entering <- has attribute + is leaving, remove attribute, prevent caret move <- does not have attribute + is entering, add attribute, prevent caret move <- not leaving or entering Inside multiple attributes using 2SCM:
-> not leaving or entering -> has attribute -> has attribute -> not leaving or entering The beginning of an element:
<- not leaving or entering <- has attribute + is leaving, remove attribute, prevent caret move The end of an element:
-> not leaving or entering -> has attribute + is leaving, remove attribute, prevent caret move Approaching from the right side:
<- approaching from the right side, remove attribute <- does not have attribute <- does not have attribute <- not leaving or entering Other questions regarding the solutionHow 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, * - 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, |
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:
How would it work?
-> keep gravity to the left -> keep gravity to the left <- keep gravity to the right <- keep gravity to the right The gravity would be also reset on 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 :)). |
…with the highest priority.
…called if the two-step called movement engages.
return true; | ||
} | ||
const { nodeBefore, nodeAfter } = position; | ||
const isAttrBefore = nodeAfter ? nodeAfter.hasAttribute( attribute ) : false; |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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? 🤔
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 🤷♂️
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Things I found in manual test (http://localhost:8125/ckeditor5-engine/tests/manual/two-step-caret.html):
|
… the boundary of an attribute should engage the two-step caret movement.
I'm unable to reproduce it.
It's because the engine sets attributes and we remove them :/
We sacrificed this when choosing
Me too. But I'm afraid there's no easy way to do that. Different |
OK, the above is not caused by 2scm. |
…tribute-preceded-by-some-text-at-the-end-of-the-block-case.
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?