We're trying to write a 'paragraph' model downcast converter that will wrap all text nodes in a span, inside the paragraph p block element.
For example we have the following:
function AddSpansToText(editor) {
editor.conversion.for('downcast').add(dispatcher => {
dispatcher.on('insert:paragraph', (evt, data, conversionApi) => {
// Remember to check whether the change has not been consumed yet and consume it.
if (!conversionApi.consumable.consume(data.item, 'insert')) {
return;
}
const { writer, mapper } = conversionApi
// Translate the position in the model to a position in the view.
const viewPosition = mapper.toViewPosition(data.range.start);
// Create a <p> element that will be inserted into the view at the `viewPosition`.
const div = writer.createContainerElement('p', { class: 'data-block' });
const span = writer.createAttributeElement('span', { class: 'data-text' });
writer.insert(writer.createPositionAt(div, 0), span);
// Bind the newly created view element to the model element so positions will map accordingly in the future.
mapper.bindElements(data.item, div);
// Add the newly created view element to the view.
writer.insert(viewPosition, div);
// Remember to stop the event propagation.
evt.stop();
});
});
}
We then register the function above as an extra plugin in the config settings as...
extraPlugins: [AddSpansToText],
This is close, however, we're not able to get the text node to appear inside the span, it appears as a peer, as ...
<p>
Text here....
<span></span>
</p>
We can't seem to map the model to the new view position.
Suggestions as to what we might be doing wrong greatly appreciated.
For anyone else looking for this, and based loosely on this example here... https://ckeditor.com/docs/ckeditor5/latest/framework/guides/deep-dive/conversion/custom-element-conversion.html
... here's what I've come up with...
/**
* Helper method to map model to view position
*
* @param {*} view
*/
function createModelToViewPositionMapper(view) {
return (evt, data) => {
const modelPosition = data.modelPosition;
const parent = modelPosition.parent;
// Only the mapping of positions that are directly in
// the <paragraph> model element should be modified.
if (!parent || !parent.is('element', 'paragraph')) {
return;
}
// Get the mapped view element <div class="data-block">.
const viewElement = data.mapper.toViewElement(parent);
// Find the <span class="data-text"> in it.
const viewContentElement = findContentViewElement( view, viewElement );
// Translate the model position offset to the view position offset.
data.viewPosition = data.mapper.findPositionIn( viewContentElement, modelPosition.offset );
};
}
/**
* Helper method to find child span at correct curser offset
*
* @param {*} editingView
* @param {*} viewElement
* @returns <span class="data-text"> nested in the parent view structure.
*/
function findContentViewElement( editingView, viewElement ) {
for ( const value of editingView.createRangeIn( viewElement ) ) {
if ( value.item.is( 'element', 'span' ) && value.item.hasClass( 'data-text' ) ) {
return value.item;
}
}
}
/**
* Paragraph model downcast converter to wrap all text nodes in
* inline span elements
*
* @param {*} editor
*/
function ParagraphConverter(editor) {
editor.conversion.for('downcast').add(dispatcher => {
dispatcher.on('insert:paragraph', (evt, data, conversionApi) => {
// Remember to check whether the change has not been consumed yet and consume it.
if (!conversionApi.consumable.consume(data.item, 'insert')) {
return;
}
const { writer, mapper } = conversionApi
// Translate the position in the model to a position in the view.
const viewPosition = mapper.toViewPosition(data.range.start);
// Create a <div> element that will be inserted into the view at the `viewPosition`.
const div = writer.createContainerElement('div', { class: 'data-block' });
// Create the <span> element that will be inserted into the div
const span = writer.createEditableElement('span', { class: 'data-text' });
writer.insert(writer.createPositionAt(div, 0), span);
// Bind the newly created view element to the model element so positions will map accordingly in the future.
mapper.bindElements(data.item, div);
// Add the newly created view element to the view.
writer.insert(viewPosition, div);
// Remember to stop the event propagation.
evt.stop();
});
});
// Dynamic mapping for model to view and curser position with correct offset
editor.editing.mapper.on( 'modelToViewPosition', createModelToViewPositionMapper( editor.editing.view ) );
editor.data.mapper.on( 'modelToViewPosition', createModelToViewPositionMapper( editor.editing.view ) );
}