-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Declarative reconversion using triggerBy #8099
Conversation
# Conflicts: # packages/ckeditor5-engine/src/conversion/downcastdispatcher.js
…sionTriggerEvent() as it is a protected method.
@scofalik I've refactored this PR. I think that it is much straightforward as it does not use differ at all to reconvert an element. |
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 didn't go through it as thoroughly as the last time. AFAICS, the things that we discussed are changed and obviously, the code looks simpler. I didn't try to analyze if all methods are correctly written. From my POV, I approve this PR, though I left some suggestions in the review.
* @type {Map<String, String>} | ||
* @private | ||
*/ | ||
this._reconversionTriggerEventToElementNameMapping = new Map(); |
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.
German people would approve this name.
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.
OK after a second thought I've renamed that a bit since it is used in a context anyway.
* @returns {Boolean} | ||
*/ | ||
_isReconvertTriggerEvent( eventName, elementName ) { | ||
return this._reconversionTriggerEventToElementNameMapping.has( eventName ) && |
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.
Hehe, once again, I think that .has() here is not needed.
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.
Cause if it is not there it will be undefined === elementName, should return false, right.
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.
Yep, you're right.
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.
Actually not - here you can have either element or text. I'll clean things up a bit here to make that clear.
return `${ type }:${ name }`; | ||
} | ||
|
||
function getParentElementFromChange( entry ) { |
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've seen this function used and it was expected to return falsy value and it was supposed to be a text node. But does it really happen? I think it will always return something, doesn't it?
|
||
function elementOrTextProxyToView( item, mapper ) { | ||
if ( item.is( 'textProxy' ) ) { | ||
const mappedPosition = mapper.toViewPosition( Position._createAt( item.parent, item.startOffset ) ); |
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 use _createBefore()
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.
At least I think so.
if ( item.is( 'textProxy' ) ) { | ||
const mappedPosition = mapper.toViewPosition( Position._createAt( item.parent, item.startOffset ) ); | ||
|
||
return mappedPosition.parent.is( '$text' ) ? mappedPosition.parent : null; |
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.
Are you sure this will always work? I didn't analyze it but it looks suspicious. Does mapper always prefer a position inside a text node? I wonder when it returns null (cause AFAICS you expect it) and if you are ready to handle it.
@@ -1349,6 +1366,21 @@ function downcastElementToElement( config ) { | |||
|
|||
return dispatcher => { | |||
dispatcher.on( 'insert:' + config.model, insertElement( config.view ), { priority: config.converterPriority || 'normal' } ); | |||
|
|||
if ( config.triggerBy ) { | |||
if ( Array.isArray( config.triggerBy.attributes ) ) { |
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 Array.isArray and not simply !config.triggerBy.attributes
?
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.
Because I wanted to be sure it is an array. But we do not test for this after all so an early Exception on weird config might be better.
Big thanks! Will take it from here with @niegowski :) |
An early Exception will tell developer that the config provided is wrong.
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.
Only some very minor things. Other than that now this code is much more readable than during my previous review 🙂
* Get changes without those that needs to be converted using {@link #reconvertElement} defined by a `triggerBy` configuration for | ||
* {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `elementToElement()`} conversion helper. |
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.
This description seems outdated.
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.
Updated, dunno if clearer 😞
Co-authored-by: Kuba Niegowski <1232187+niegowski@users.noreply.github.com>
Suggested merge commit message (convention)
Feature (engine): Introduce automatic model-to-view reconversion by defining
triggerBy
option forelementToElement()
conversion helper. Closes #7956.Other (table): Table cell contents refreshing for the editing view will now make less view updates.
Additional information
This PR:
triggerBy
configuration option for downcastelementToElement()
conversion helper config.DowncastDispatcher
level.table-cell-refresh-post-fixer
to only re-insert paragraphs not entire table cells.This PR:Introduces'refresh'
type to differ changes (internally represented as'x'
in differ processing algorithms).This is needed to properly handle refreshing items by the dispatcher. Assumptions:The refreshing newly inserted element should be treated as'insert'
Attribute changes on refreshed item must be preserved - allows to have atomic converters for attributes working alongside refreshing.Introduces automatic refreshing of an item inelementToElement
downcast by a new configuration optiontriggerBy
.It defines which inner events should trigger refreshing. This has limited support for now (follow-up). You can trigger reconversion of the element on attribute change or by insert/remove of a child. This process does not cover three-way-binding of a layerd view elements (follow-up, case ofimage -> figure>img
conversion).IntroducesDowncastDispatcher.convertRefresh()
that handles refreshing elements withmemoization.The memoization process is done by re-using existing child view elements. For instance refreshing<paragraph>
will trigger'insert'
+'attribute'
conversion events for the main element and will use existing view elements for child text nodes and elements. It will also trigger proper events for newly added child elements.RenamesDiffer.refreshItem()
toDiffer.reinsertItem()
and will deprecate it (follow-up).The name is needed for the new'refresh'
change handling. ThereinsertItem()
is still needed astriggerBy
doesn't handle table case (missing three-way-binding).Re-introducesDiffer.refreshItem()
that now uses'refresh'
change instead of'remove'
+'insert'
combo (reserved forDiffer.reinsertItem()
.The table'stable-cell-refresh-post-fixer
logic was changed to refresh only<paragraph>
elements inside a table cell.It uses newDiffer.refreshItem()
in order to make smaller changes in the view. The logic behind was also changed as I found a different approach to it.For this matter, I've changed how<paragraph>
inside<tableCell>
is converted.There's new converter for a<paragraph>
with a'high'
priority that overrides default conversion for table case (so it can be rendered as<span>
in theediting
view. This converter is run onrefreshItem()
as well.Internals of the differ has beenslightlyrefactored so parts of the logic of inserting an item could be re-used in refreshing mechanism. However, I did not review all the changes there and I feel that some might be removed. For instance, I've addedrefresh x insert|remove|attribute
changes handling in theDiffer._handleChange()
later than some of the earlier changes.TODO before merge:
downcast.js
artilce.js
manual testarticle.js
)mapper.bindSlots()
is needed