Skip to content
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

Merged
merged 119 commits into from
Oct 13, 2020
Merged

Declarative reconversion using triggerBy #8099

merged 119 commits into from
Oct 13, 2020

Conversation

jodator
Copy link
Contributor

@jodator jodator commented Sep 17, 2020

Suggested merge commit message (convention)

Feature (engine): Introduce automatic model-to-view reconversion by defining triggerBy option for elementToElement() conversion helper. Closes #7956.

Other (table): Table cell contents refreshing for the editing view will now make less view updates.


Additional information

This PR:

  • introduces the triggerBy configuration option for downcast elementToElement() conversion helper config.
  • Handles element re-conversion and view momoization on DowncastDispatcher level.
    • The model element children that have exiting view are not reconverted (memoization).
    • Newly inserted children are converted by corresponding converters.
    • Converter for reconverted element can converts also its children (especially in case of layered view)
  • Has fixes for table package table-cell-refresh-post-fixer to only re-insert paragraphs not entire table cells.

This PR:

  1. 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:
    1. The refreshing newly inserted element should be treated as 'insert'
    2. Attribute changes on refreshed item must be preserved - allows to have atomic converters for attributes working alongside refreshing.
  2. Introduces automatic refreshing of an item in elementToElement downcast by a new configuration option triggerBy.
    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 of image -> figure>img conversion).
  3. Introduces DowncastDispatcher.convertRefresh() that handles refreshing elements with memoization.
    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.
  4. Renames Differ.refreshItem() to Differ.reinsertItem() and will deprecate it (follow-up).
    The name is needed for the new 'refresh' change handling. The reinsertItem() is still needed as triggerBy doesn't handle table case (missing three-way-binding).
  5. Re-introduces Differ.refreshItem() that now uses 'refresh' change instead of 'remove' + 'insert' combo (reserved for Differ.reinsertItem().
  6. The table's table-cell-refresh-post-fixer logic was changed to refresh only <paragraph> elements inside a table cell.
    It uses new Differ.refreshItem() in order to make smaller changes in the view. The logic behind was also changed as I found a different approach to it.
  7. 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 the editing view. This converter is run on refreshItem() as well.

Internals of the differ has been slightly refactored 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 added refresh x insert|remove|attribute changes handling in the Differ._handleChange() later than some of the earlier changes.


TODO before merge:

  • review the code with @niegowski
    • extract paragraph in table cell converter to downcast.js
    • check todos
    • fix artilce.js manual test
    • create re-conversion manual test (current article.js)
    • check if mapper.bindSlots()is needed
    • refreshing table cell paragraph might cause infinite loops (Writing post-fixers is annoying #1936 (comment))
  • review differ / conversion changes with @scofalik after the initial review.
  • check if the memoization is done properly (it looks like I have this on two levels)

# Conflicts:
#	packages/ckeditor5-engine/src/conversion/downcastdispatcher.js
@jodator
Copy link
Contributor Author

jodator commented Oct 12, 2020

@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.

@jodator jodator requested a review from scofalik October 12, 2020 14:59
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.

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();
Copy link
Contributor

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.

Copy link
Contributor Author

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 ) &&
Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, you're right.

Copy link
Contributor Author

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 ) {
Copy link
Contributor

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 ) );
Copy link
Contributor

Choose a reason for hiding this comment

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

You can use _createBefore()

Copy link
Contributor

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;
Copy link
Contributor

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 ) ) {
Copy link
Contributor

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 ?

Copy link
Contributor Author

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.

@jodator
Copy link
Contributor Author

jodator commented Oct 13, 2020

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.

Big thanks! Will take it from here with @niegowski :)

An early Exception will tell developer that the config provided is wrong.
Copy link
Contributor

@niegowski niegowski left a 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 🙂

Comment on lines 594 to 595
* 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.
Copy link
Contributor

Choose a reason for hiding this comment

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

This description seems outdated.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated, dunno if clearer 😞

jodator and others added 2 commits October 13, 2020 15:40
Co-authored-by: Kuba Niegowski <1232187+niegowski@users.noreply.github.com>
@niegowski niegowski merged commit a7c9973 into master Oct 13, 2020
@niegowski niegowski deleted the i/7956 branch October 13, 2020 14:28
@jodator jodator changed the title 7956: Differ.refreshItem() + triggerBy Declarative reconversion using triggerBy Oct 14, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

A triggerBy option that issues element re-render
4 participants