CKEditor 5 Downcast Converter for Paragraph To Wrap Text in Span

2.2k Views Asked by At

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.

1

There are 1 best solutions below

0
On BEST ANSWER

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 ) );
}