ckeditor / ckeditor5

Powerful rich text editor framework with a modular architecture, modern integrations, and features like collaborative editing.
https://ckeditor.com/ckeditor-5
Other
8.69k stars 3.62k forks source link

How to render track changes on custom elements? #16503

Open DeltekDavid opened 3 weeks ago

DeltekDavid commented 3 weeks ago

📝 Ask a question

We are using markInlineFormat to customize track change behavior with our inline widgets. For example, we have a widget that displays units of measure, like "3 in (7.62 cm)". We created a custom command that lets the user set these values via a balloon form, and executes our setUnitsOfMeasure command. We customized it so we can have a nice description like "Set units of measure to 2 in (5.08 cm)".

However, per the docs, the model state doesn't actually update until you accept the track change. Thus, our converter still renders "3 in (7.72 cm)" in this case even though you changed it to 2 inches.

How can we take the track changes into account when rendering our widget, so that it shows the value we want? In this case, we want to display it as though we had used the default attribute integration (which won't cut it for us, since it doesn't let us show more than the attribute name in the track comment). I got lost somewhere in the Marker documentation. Thanks!

scofalik commented 3 weeks ago

Hi @DeltekDavid.

Two questions for start:

  1. Which editor version do you use?
  2. How exactly do you set these measures? Do you keep them as model attributes?

If you use CKE5 v40.1.0 or higher and setUnitsofMeasure command changes just model attributes, I'd highly recommend switching to using attributes track changes integration. Not only should it simplify your code, but the changes will be instantly reflected in the content, solving your issue and providing a better UX.

Using markInlineFormat() and markBlockFormat() should be the last resort now, so please first check if it is possible to you to migrate to the new solution (attribute TC integration was introduced in v40.1.0).

DeltekDavid commented 3 weeks ago

Hi @scofalik , apologies--I run the latest nightly build (it's a research POC, and we need the new installation method).

I tried using the attribute integration as I mentioned, but it lacks the ability to supply descriptive TC comments. For example, it would only show "changed: metric" or similar, rather than let me put a custom comment like "Set units to X in (Y cm)". Is there a way to customize the comment beyond just the attribute label via registerAttributeLabel?

I tried using registerDescriptionCallback like below, but the TC comment just shows "Set format: metric, imperial".

trackChangesPlugin.enableDefaultAttributesIntegration('modifySelectedUnitsOfMeasure');
trackChangesPlugin.registerInlineAttribute('metric');
trackChangesPlugin.registerInlineAttribute('imperial');

// Create track change descriptions for units-of-measure changes.
trackChangesPlugin.descriptionFactory.registerDescriptionCallback(suggestion => {
    const { data } = suggestion;

    if (!data || data.commandName !== 'modifySelectedUnitsOfMeasure') {
        return;
    }

    const imperial = data.commandParams?.[0]?.imperial;
    const metric = data.commandParams?.[0]?.metric;
    const content = `Changed units of measure to "${imperial} (${metric})"`;

    return {
        type: 'format',
        content
    };
});

The custom description works, however, when I use an enableCommand callback rather than enableDefaultAttributesIntegration and registerInlineAttribute. (But then we don't get the model updates, which is the impetus for my question). Thanks

scofalik commented 3 weeks ago

Sorry, I missed that you tried attribute suggestions.

For attribute suggestions, we store different properties in data. They are directly connected with the attribute change, not with the command. The data has { key, oldValue, newValue }. Also, there will be now two suggestions, one for imperial and one for metric attribute (as I understand, you have two attributes that you change simultaneously).

Try this:

trackChangesPlugin.descriptionFactory.registerDescriptionCallback(suggestion => {
    const { data } = suggestion;

    if ( !data ) {
        return;
    }

    if ( data.attributeName == 'metric' ) {
        // Skip `metric` attribute.
        // The description will be generated only for `imperial` attribute.
        return { type: 'format', content: '' };
    }

    if ( data.attributeName == 'imperial' ) {
        const content = `Changed units of measure to "${imperial} (${imperial*2.54})"`;

        return { type: 'format', content };
    }
} );

As I wrote, I assume that metric and imperial values are changed at the same time, and their values are related. You may want to use some util instead of just doing * 2.54.

BTW: If these values are always related, I'd advise just keeping one in the model as it is unnecessary data duplication.

BTW 2: I see that you used registerInlineAttribute. The docs may be a bit misleading here. I think that you should have used registerBlockAttribute as the attribute is on widget. Maybe the methods names are a bit unfortunate here, but basically, AFAIR, registerInlineAttribute = text, registerBlockAttribute = non-text.

BTW 3: If you don't want to use imperial attribute to calculate metric value, you may try to read the metric value straight from the model: suggestion.getFirstRange().start.nodeAfter.getAttribute( 'metric' ) should work

BTW 4: I am writing this from top of my head without testing, so please make sure to test any customizations on your own.

DeltekDavid commented 3 weeks ago

Thank you @scofalik ! That did the trick. The custom feature TC docs could use this information.

BTW I stuck with registerInlineAttribute because I liked how it showed a formatting highlight around the entire inline widget. When I used registerBlockAttribute, it still worked but only the beginning of my widget showed a vertical line indicating formatting change.

Updated code:

// Track changes to units of measure.
trackChangesPlugin.enableDefaultAttributesIntegration('modifySelectedUnitsOfMeasure');
trackChangesPlugin.registerInlineAttribute('metric');
trackChangesPlugin.registerInlineAttribute('imperial');

// Create friendly TC descriptions for units of measure changes.
trackChangesPlugin.descriptionFactory.registerDescriptionCallback(suggestion => {
    const data = suggestion.data;
    if (!data) {
        return;
    }

    const attributeName = data.key;
    if (attributeName === 'metric') {
        // Skip `metric` attribute.
        // The description will be generated only for `imperial` attribute.
        return { type: 'format', content: '' };
    }

    if (attributeName === 'imperial') {
        const imperial = data.newValue;
        const metric = suggestion.getFirstRange()?.start?.nodeAfter?.getAttribute('metric');
        const content = `Changed units of measure to "${imperial} (${metric})"`;
        return { type: 'format', content };
    }
});