|
|
@@ -1,41 +1,45 @@
|
|
|
---
|
|
|
category: framework-deep-dive-conversion
|
|
|
menu-title: Custom element conversion
|
|
|
-order: 30
|
|
|
+order: 40
|
|
|
---
|
|
|
|
|
|
{@snippet framework/build-custom-element-converter-source}
|
|
|
|
|
|
-## Intro
|
|
|
+There are three levels on which elements can be converted:
|
|
|
|
|
|
-Let's assume that content in your application contains "info boxes". As for now it was only required to wrap part of a content in a `<div>` element:
|
|
|
+* By using the two-way converter: {@link module:engine/conversion/conversion~Conversion#elementToElement `conversion.elementToElement()`}. It is a fully declarative API. It is the least powerful option but it is the easiest one to use.
|
|
|
+* By using one-way converters: for example {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `conversion.for( 'downcast' ).elementToElement()`} and {@link module:engine/conversion/upcasthelpers~UpcastHelpers#elementToElement `conversion.for( 'upcast' ).elementToElement()`}. In this case, you need to define at least two converters (for upcast and downcast), but the "how" part becomes a callback, and hence you gain more control over it.
|
|
|
+* Finally, by using event-based converters. In this case, you need to listen to events fired by {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} and {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher}. This method has the full access to every bit of logic that a converter needs to implement and therefore it can be used to write the most complex conversion methods.
|
|
|
|
|
|
-<!-- Optional: Image of a simple info box -->
|
|
|
+In this guide, we will show you how to migrate from a simple two-way converter to an event-based converters as the requirements regarding the feature get more complex.
|
|
|
|
|
|
-The HTML structure of info box:
|
|
|
+## Introduction
|
|
|
+
|
|
|
+Let's assume that content in your application contains "info boxes". As for now, it was only required to wrap part of a content in a `<div>` element that would look in the data and editing views like this:
|
|
|
|
|
|
```html
|
|
|
<div class="info-box">
|
|
|
<!-- any editable content -->
|
|
|
- <p>This is<strong>important!</strong></p>
|
|
|
+ <p>This is <strong>important!</strong></p>
|
|
|
</div>
|
|
|
```
|
|
|
|
|
|
-This will be stored in the model as:
|
|
|
+This data is represented in the model as the following structure:
|
|
|
|
|
|
```html
|
|
|
<infoBox>
|
|
|
- <!-- any $block, like: -->
|
|
|
- <paragraph><$text>This is</$text><$text bold="true">important!</$text></paragraph>
|
|
|
+ <!-- any $block content: -->
|
|
|
+ <paragraph><$text>This is </$text><$text bold="true">important!</$text></paragraph>
|
|
|
</infoBox>
|
|
|
```
|
|
|
|
|
|
-This can be easily done with below schema and converters in a simple `InfoBox` plugin:
|
|
|
+This can be easily done with the below schema and converters in a simple `InfoBox` plugin:
|
|
|
|
|
|
```js
|
|
|
class InfoBox {
|
|
|
constructor( editor ) {
|
|
|
- // 1. Define infoBox as a object that can be contain any other content.
|
|
|
+ // 1. Define infoBox as an object that can be contained any other content.
|
|
|
editor.model.schema.register( 'infoBox', {
|
|
|
allowWhere: '$block',
|
|
|
allowContentOf: '$root',
|
|
|
@@ -54,67 +58,72 @@ class InfoBox {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-## Moving from general converters to an event based
|
|
|
+## Migrating to an event-based converter
|
|
|
|
|
|
-### Updated data & model structure
|
|
|
+Let's now assume that the requirements have changed and there is a need for adding an additional element in the data and editing views that will display the type of the info box (warning, error, info, etc.).
|
|
|
|
|
|
-This basic definition allows to nest some part of a content inside a pre-defined "info-box". However, with time, the requirements has changed and there's a need for adding a set of types of info boxes. The new info box will have a structure:
|
|
|
+The new info box structure:
|
|
|
|
|
|
```html
|
|
|
<div class="info-box info-box-warning">
|
|
|
<div class="info-box-title">Warning</div>
|
|
|
<div class="info-box-content">
|
|
|
<!-- any editable content -->
|
|
|
- <p>This is<strong>important!</strong></p>
|
|
|
+ <p>This is <strong>important!</strong></p>
|
|
|
</div>
|
|
|
</div>
|
|
|
```
|
|
|
|
|
|
-As the "Warning" shouldn't be editable and defines a type of the info box we can store this as an attribute of a `<infoBox>` element:
|
|
|
+The "Warning" part should not be editable. It defines a type of the info box so we can store this bit of information as an attribute of the `<infoBox>` element:
|
|
|
|
|
|
```html
|
|
|
<infoBox infoBoxType="warning">
|
|
|
- <!-- any $block, like: -->
|
|
|
- <paragraph><$text>This is</$text><$text bold="true">important!</$text></paragraph>
|
|
|
+ <!-- any $block content: -->
|
|
|
+ <paragraph><$text>This is </$text><$text bold="true">important!</$text></paragraph>
|
|
|
</infoBox>
|
|
|
```
|
|
|
|
|
|
+Let's see how to update our basic implementation to cover these requirements.
|
|
|
+
|
|
|
### Demo
|
|
|
|
|
|
-Below is a demo of the editor with example info box.
|
|
|
+Below is a demo of the editor with the example info box.
|
|
|
|
|
|
{@snippet framework/extending-content-custom-element-converter}
|
|
|
|
|
|
-### Updated model schema
|
|
|
+### Schema
|
|
|
|
|
|
-The type of the box is defined by the additional class to the `"info-box"` on main `<div>` but it is also visible as text in `<div class="info-box-title">`. All the info box content must be placed in `<div class="info-box-content">`.
|
|
|
+The type of the box is defined by the additional class on the main `<div>` but it is also represented as text in `<div class="info-box-title">`. All the info box content must be now placed inside `<div class="info-box-content">` instead of the main wrapper.
|
|
|
|
|
|
-For the above requirements we can see that the model structure of the `infoBox` doesn't need to change much. The only addition to the model is an attribute that will hold information of the info box type:
|
|
|
+For the above requirements we can see that the model structure of the `infoBox` does not need to change much. We can still use a single element in the model. The only addition to the model is an attribute that will hold information about the info box type:
|
|
|
|
|
|
```js
|
|
|
editor.model.schema.register( 'infoBox', {
|
|
|
allowWhere: '$block',
|
|
|
allowContentOf: '$root',
|
|
|
isObject: true,
|
|
|
- allowedAttributes: [ 'infoBoxType' ] // Added
|
|
|
+ allowAttributes: [ 'infoBoxType' ] // Added
|
|
|
} );
|
|
|
```
|
|
|
|
|
|
-### Event based element to element upcast converter
|
|
|
+### Event-based upcast converter
|
|
|
+
|
|
|
+The conversion of the type of the box itself could be achieved by using {@link module:engine/conversion/conversion~Conversion#attributeToAttribute `attributeToAttribute()`} (`info-box-*` CSS classes to the `infoBoxType` model attribute). However, two more changes were made to the data format that we need to handle:
|
|
|
|
|
|
-The info box type conversion can be achieved using `attributeToAttribute` converter by converting `info-box-*` css classes to `infoBoxType` attribute. However, adding non-converted layers of the content requires to write an event based converter to have better control over conversion process.
|
|
|
+* There is the new `<div class="info-box-title">` element that should be ignored during upcast conversion as it duplicates the information conveyed by the main element's CSS class.
|
|
|
+* The content of the info box is now located inside another element (previously it was located directly in the main wrapper).
|
|
|
|
|
|
-For the proposed HTML structure we need to:
|
|
|
+Neither two-way nor one-way converters can handle such conversion. Therefore, we need to use an event-based converter with the following behavior:
|
|
|
|
|
|
-1. Create model's `<infoBox>` element with `infoBoxType` attribute.
|
|
|
-1. Skip conversion of `<div class="info-box-title">` as the information about type can be obtained from the main info-box CSS classes.
|
|
|
-1. Convert children of `<div class="info-box-content">` and insert them into model's `<infoBox>`.
|
|
|
+1. Create model `<infoBox>` element with `infoBoxType` attribute.
|
|
|
+1. Skip conversion of `<div class="info-box-title">` as the information about type can be obtained from the wrapper's CSS classes.
|
|
|
+1. Convert children of `<div class="info-box-content">` and insert them directly into `<infoBox>`.
|
|
|
|
|
|
```js
|
|
|
function upcastConverter( event, data, conversionApi ) {
|
|
|
const viewInfoBox = data.viewItem;
|
|
|
|
|
|
- // Detect that view element is an info-box div.
|
|
|
+ // Check whether the view element is an info box <div>.
|
|
|
// Otherwise, it should be handled by another converter.
|
|
|
if ( !viewInfoBox.hasClass( 'info-box' ) ) {
|
|
|
return;
|
|
|
@@ -125,39 +134,42 @@ function upcastConverter( event, data, conversionApi ) {
|
|
|
infoBoxType: getTypeFromViewElement( viewInfoBox )
|
|
|
} );
|
|
|
|
|
|
- // Try to safely insert element - if it returns false the element can't be safely inserted
|
|
|
+ // Try to safely insert the element into the model structure.
|
|
|
+ // If `safeInsert()` returns `false` the element cannot be safely inserted
|
|
|
// into the content, and the conversion process must stop.
|
|
|
+ // This may happen if the data that we are converting has incorrect structure
|
|
|
+ // (e.g. was copied from an external website).
|
|
|
if ( !conversionApi.safeInsert( modelElement, data.modelCursor ) ) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // Mark info-box div as handled by this converter.
|
|
|
+ // Mark the info box <div> as handled by this converter.
|
|
|
conversionApi.consumable.consume( viewInfoBox, { name: true } );
|
|
|
|
|
|
// Let's assume that the HTML structure is always the same.
|
|
|
+ // Note: for full bulletproofing this converter we should also check
|
|
|
+ // whether these elements are the right ones.
|
|
|
const viewInfoBoxTitle = viewInfoBox.getChild( 0 );
|
|
|
const viewInfoBoxContent = viewInfoBox.getChild( 1 );
|
|
|
|
|
|
- // Mark info-box inner elements as handled by this converter.
|
|
|
+ // Mark info box inner elements (title and content <div>s) as handled by this converter.
|
|
|
conversionApi.consumable.consume( viewInfoBoxTitle, { name: true } );
|
|
|
conversionApi.consumable.consume( viewInfoBoxContent, { name: true } );
|
|
|
|
|
|
- // Let the editor handle children of the info-box content conversion.
|
|
|
+ // Let the editor handle children of <div class="info-box-content">.
|
|
|
conversionApi.convertChildren( viewInfoBoxContent, modelElement );
|
|
|
|
|
|
- // Conversion requires updating result data structure properly.
|
|
|
+ // Finally, update the conversion's modelRange and modelCursor.
|
|
|
conversionApi.updateConversionResult( modelElement, data );
|
|
|
}
|
|
|
|
|
|
-// Helper funciton to read type form the view classes.
|
|
|
+// Helper function to read the type from the view classes.
|
|
|
function getTypeFromViewElement( viewElement ) {
|
|
|
- const classNames = [ ...viewElement.getClassNames() ];
|
|
|
-
|
|
|
- if ( classNames.includes( 'info-box-info' ) ) {
|
|
|
+ if ( viewElement.hasClass( 'info-box-info' ) ) {
|
|
|
return 'Info';
|
|
|
}
|
|
|
|
|
|
- if ( classNames.includes( 'info-box-warning' ) ) {
|
|
|
+ if ( viewElement.hasClass( 'info-box-warning' ) ) {
|
|
|
return 'Warning';
|
|
|
}
|
|
|
|
|
|
@@ -165,18 +177,29 @@ function getTypeFromViewElement( viewElement ) {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-### Accompanying downcast conversion & updated plugin code
|
|
|
+This upcast converter callback can now be plugged by adding a listener to the {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher#element `UpcastDispatcher#element` event}. We will listen to `element:div` to ensure that the callback is called only for `<div>` elements.
|
|
|
+
|
|
|
+```js
|
|
|
+editor.conversion.for( 'upcast' )
|
|
|
+ .add( dispatcher => dispatcher.on( 'element:div', upcastConverter ) );
|
|
|
+```
|
|
|
+
|
|
|
+### Event-based downcast converter
|
|
|
+
|
|
|
+The missing bit are the downcast converters for the editing and data pipelines. We want to use the widget system to make the info box behave like an "object". The other aspect that we need to take care of is the fact that the view structure has more elements than the model structure. In this case, we could actually use one-way converters. However, we will showcase how an event-based converter would look.
|
|
|
|
|
|
<info-box>
|
|
|
- See the {@link framework/guides/tutorials/implementing-a-block-widget Implementing a block widget} to learn about widget system concepts.
|
|
|
+ See the {@link framework/guides/tutorials/implementing-a-block-widget Implementing a block widget} to learn about the widget system.
|
|
|
</info-box>
|
|
|
|
|
|
+The remaining downcast converters:
|
|
|
+
|
|
|
```js
|
|
|
function editingDowncastConverter( event, data, conversionApi ) {
|
|
|
let { infoBox, infoBoxContent, infoBoxTitle } = createViewElements( data, conversionApi );
|
|
|
|
|
|
- // Decorate view items as widgets.
|
|
|
- infoBox = toWidget( infoBox, conversionApi.writer, { label: 'simple box widget' } );
|
|
|
+ // Decorate view items as a widget and widget editable area.
|
|
|
+ infoBox = toWidget( infoBox, conversionApi.writer, { label: 'info box widget' } );
|
|
|
infoBoxContent = toWidgetEditable( infoBoxContent, conversionApi.writer );
|
|
|
|
|
|
insertViewElements( data, conversionApi, infoBox, infoBoxTitle, infoBoxContent );
|
|
|
@@ -223,7 +246,11 @@ function insertViewElements( data, conversionApi, infoBox, infoBoxTitle, infoBox
|
|
|
infoBoxContent
|
|
|
);
|
|
|
|
|
|
+ // The default mapping between the model <infoBox> and its view representation.
|
|
|
conversionApi.mapper.bindElements( data.item, infoBox );
|
|
|
+ // However, since the model <infoBox> content need to end up in the inner
|
|
|
+ // <div class="info-box-content"> we need to bind one with another overriding
|
|
|
+ // part of the default binding.
|
|
|
conversionApi.mapper.bindElements( data.item, infoBoxContent );
|
|
|
|
|
|
conversionApi.writer.insert(
|
|
|
@@ -233,7 +260,18 @@ function insertViewElements( data, conversionApi, infoBox, infoBoxTitle, infoBox
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-The updated `InfoBox` plugin:
|
|
|
+These two converters need to be plugged as listeners to the {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#insert `DowncastDispatcher#insert` event}:
|
|
|
+
|
|
|
+```js
|
|
|
+editor.conversion.for( 'editingDowncast' )
|
|
|
+ .add( dispatcher => dispatcher.on( 'insert:infoBox', editingDowncastConverter ) );
|
|
|
+editor.conversion.for( 'dataDowncast' )
|
|
|
+ .add( dispatcher => dispatcher.on( 'insert:infoBox', dataDowncastConverter ) );
|
|
|
+```
|
|
|
+
|
|
|
+### Updated plugin code
|
|
|
+
|
|
|
+The updated `InfoBox` plugin that glues all this together:
|
|
|
|
|
|
```js
|
|
|
class InfoBox {
|
|
|
@@ -257,4 +295,16 @@ class InfoBox {
|
|
|
.add( dispatcher => dispatcher.on( 'insert:infoBox', dataDowncastConverter ) );
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+function upcastConverter() {
|
|
|
+ // ...
|
|
|
+}
|
|
|
+
|
|
|
+function editingDowncastConverter() {
|
|
|
+ // ...
|
|
|
+}
|
|
|
+
|
|
|
+function dataDowncastConverter() {
|
|
|
+ // ...
|
|
|
+}
|
|
|
```
|