Track Changes: Does registerInlineAttribute() work with attributes of nested elements?
📝 Ask a question
Trying to get Track Changes default attribute integration working with a nested element's attributes.
We have a outer element called bracketOption, represented by an inline button widget. It has an attribute, optedState.
The bracketOption has a nested inline element called nestedText, with an attribute called value. (We do it this way because the brackets can have multiple nested content of different types. Consider just the simple case of a single nestedText child, however.)
When you click the bracketOption button, two things happen:
- The
optedStateattribute gets set toOPTED_INvia ourtoggleBracketOptionCommand. This receives a tracked change, all good. - The user is prompted to fill in a new value for
nestedText.value, similar to the Abbreviation demo. When they hit OK (green check), we executemodifyBracketOptionValueCommandand set thevalueof the first (and in this case only)nestedTextelement. This does NOT get a tracked change!
Schema definition:
_defineSchema() {
const schema = this.editor.model.schema
schema.register('bracketOption', {
// Behaves like a self-contained inline object (e.g. an inline image)
// allowed in places where $text is allowed (e.g. in paragraphs).
inheritAllFrom: '$inlineObject',
allowAttributes: [
'id',
'optedState' // 'undecided', 'optedIn', or 'optedOut'
]
})
schema.register('nestedUnitsOfMeasure', { // not used in this example
inheritAllFrom: '$inlineObject',
allowAttributes: ['imperial', 'metric']
})
// Use a custom element for nested text, since regular text nested under a bracket causes extra text to render/
// (Regular text is automatically converted to a text node alongside the bracket's own React component.)
schema.register('nestedText', {
inheritAllFrom: '$inlineObject',
allowAttributes: ['value'],
})
}
TC integration:
enableTrackChangeIntegration(trackChangesPlugin) {
// Track toggling of bracket options.
trackChangesPlugin.enableDefaultAttributesIntegration('toggleBracketOption');
trackChangesPlugin.registerInlineAttribute('optedState');
// Create friendly TC descriptions for bracket option toggles.
trackChangesPlugin.descriptionFactory.registerDescriptionCallback(suggestion => {
const data = suggestion.data;
if (!data) {
return;
}
const attributeName = data.key;
if (attributeName !== 'optedState') {
return;
}
const optedState = data.newValue;
const value = suggestion.getFirstRange()?.start?.nodeAfter?.getChild(0)?.getAttribute('value');
const content = optedState === 'OPTED_IN'
? `Selected option: "${value}"`
: optedState === 'OPTED_OUT' ? `Deselected option: "${value}"`
: `Reset option: "${value}"`;
return { type: 'format', content };
});
// Track setting of fill-in-the-blank values.
trackChangesPlugin.enableDefaultAttributesIntegration('modifyBracketOptionValue');
trackChangesPlugin.registerInlineAttribute('value');
// Create friendly TC descriptions for setting fill-in-the-blank values.
// (Note: This code never gets called since we can't get TC to work at all with the nestedText)
trackChangesPlugin.descriptionFactory.registerDescriptionCallback(suggestion => {
const data = suggestion.data;
if (!data) {
return;
}
const attributeName = data.key;
if (attributeName !== 'value') {
return;
}
const value = data.newValue;
const content = `Set custom option value: "${value}"`;
return { type: 'format', content };
});
}
Commands:
export default class ToggleBracketOptionCommand extends Command {
execute({ bracketOptionId, _, newState }) {
const model = this.editor.model;
model.change(writer => {
const root = model.document.getRoot();
if (!root) {
return;
}
const range = model.createRangeIn(root);
const bracketOptionElement = getItemByAttributeValue(range, 'id', bracketOptionId);
if (!bracketOptionElement) {
return;
}
writer.setAttribute('optedState', newState, bracketOptionElement)
});
}
refresh() {
const model = this.editor.model;
const selection = model.document.selection;
// The command is enabled when the "optedState" attribute
// can be set on the current model selection.
this.isEnabled = model.schema.checkAttributeInSelection(
selection, 'optedState'
);
}
}
export default class ModifyBracketOptionValueCommand extends Command {
execute({ bracketOptionId, _, newValue }) {
const model = this.editor.model;
model.change(writer => {
const root = model.document.getRoot();
if (!root) {
return;
}
const range = model.createRangeIn(root);
const bracketOptionElement = getItemByAttributeValue(range, 'id', bracketOptionId);
if (!bracketOptionElement || !bracketOptionElement.getAttribute('isEditable')) {
return;
}
// Replacement (fill-in-the-blank) options have a child element with the value; update that.
const valueChild = bracketOptionElement.getChild(0);
if (valueChild) {
writer.setAttribute('value', newValue, valueChild)
}
});
}
refresh() {
const model = this.editor.model;
const selection = model.document.selection;
// The command is enabled when the "value" attribute
// can be set on the current model selection.
// Note: Also tried having this always return true.
// Command executes successfully as evidenced by the UI and CKInspector.
// Just can't get it to work with TC attribute integration.
this.isEnabled = model.schema.checkAttributeInSelection(
selection, 'value'
);
}
}
We're using the nightly build because we need the new installation method. Specifically, our package-lock.json says 0.0.0-nightly-20240605.1 . Using NodeJS 20.9.0.
Also tried registerBlockAttribute for value, instead of registerInlineAttribute.
There has been no activity on this issue for the past year. We've marked it as stale and will close it in 30 days. We understand it may still be relevant, so if you're interested in the solution, leave a comment or reaction under this issue.
We've closed your issue due to inactivity. We understand that the issue may still be relevant. If so, feel free to open a new one (and link this issue to it).