Skip to content

Commit

Permalink
Merge stable into master
Browse files Browse the repository at this point in the history
  • Loading branch information
CKTravisBot authored Apr 14, 2021
2 parents 4a663d7 + 0851b87 commit 9fd389a
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ class InfoBox {
.add( dispatcher => dispatcher.on( 'insert:infoBox', editingDowncastConverter ) );
editor.conversion.for( 'dataDowncast' )
.add( dispatcher => dispatcher.on( 'insert:infoBox', dataDowncastConverter ) );

// Model to view position mapper is needed since the model <infoBox> content needs to end up in the inner
// <div class="info-box-content">.
editor.editing.mapper.on( 'modelToViewPosition', createModelToViewPositionMapper( editor.editing.view ) );
editor.data.mapper.on( 'modelToViewPosition', createModelToViewPositionMapper( editor.editing.view ) );
}
}

Expand Down Expand Up @@ -123,19 +128,58 @@ function insertViewElements( data, conversionApi, infoBox, infoBoxTitle, infoBox
infoBoxContent
);

// The default mapping between the model <infoBox> and its view representation.
// The mapping between the model <infoBox> and its view representation.
conversionApi.mapper.bindElements( data.item, infoBox );
// However, since the model <infoBox> content needs to end up in the inner
// <div class="info-box-content">, you need to bind one with another overriding
// a part of the default binding.
conversionApi.mapper.bindElements( data.item, infoBoxContent );

conversionApi.writer.insert(
conversionApi.mapper.toViewPosition( data.range.start ),
infoBox
);
}

function createModelToViewPositionMapper( view ) {
return ( evt, data ) => {
const modelPosition = data.modelPosition;
const parent = modelPosition.parent;

// Only mapping of positions that are directly in
// the <infoBox> model element should be modified.
if ( !parent.is( 'element', 'infoBox' ) ) {
return;
}

// Get the mapped view element <div class="info-box">.
const viewElement = data.mapper.toViewElement( parent );

// Find the <div class="info-box-content"> 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 );
};
}

// Returns the <div class="info-box-content"> nested in the info box view structure.
function findContentViewElement( editingView, viewElement ) {
for ( const value of editingView.createRangeIn( viewElement ) ) {
if ( value.item.is( 'element', 'div' ) && value.item.hasClass( 'info-box-content' ) ) {
return value.item;
}
}
}

function getTypeFromViewElement( viewElement ) {
if ( viewElement.hasClass( 'info-box-info' ) ) {
return 'Info';
}

if ( viewElement.hasClass( 'info-box-warning' ) ) {
return 'Warning';
}

return 'None';
}

ClassicEditor
.create( document.querySelector( '#editor-custom-element-converter' ), {
cloudServices: CS_CONFIG,
Expand All @@ -160,15 +204,3 @@ ClassicEditor
.catch( err => {
console.error( err.stack );
} );

function getTypeFromViewElement( viewElement ) {
if ( viewElement.hasClass( 'info-box-info' ) ) {
return 'Info';
}

if ( viewElement.hasClass( 'info-box-warning' ) ) {
return 'Warning';
}

return 'None';
}
Original file line number Diff line number Diff line change
Expand Up @@ -251,12 +251,8 @@ function insertViewElements( data, conversionApi, infoBox, infoBoxTitle, infoBox
infoBoxContent
);

// The default mapping between the model <infoBox> and its view representation.
// The mapping between the model <infoBox> and its view representation.
conversionApi.mapper.bindElements( data.item, infoBox );
// However, since the model <infoBox> content needs to end up in the inner
// <div class="info-box-content">, you need to bind one with another overriding
// a part of the default binding.
conversionApi.mapper.bindElements( data.item, infoBoxContent );

conversionApi.writer.insert(
conversionApi.mapper.toViewPosition( data.range.start ),
Expand All @@ -265,7 +261,7 @@ function insertViewElements( data, conversionApi, infoBox, infoBoxTitle, infoBox
}
```

These two converters need to be plugged as listeners to the {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#insert `DowncastDispatcher#insert` event}:
These two converters need to be plugged as listeners into the {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#insert `DowncastDispatcher#insert` event}:

```js
editor.conversion.for( 'editingDowncast' )
Expand All @@ -274,6 +270,83 @@ editor.conversion.for( 'dataDowncast' )
.add( dispatcher => dispatcher.on( 'insert:infoBox', dataDowncastConverter ) );
```

Due to the fact that the info box's view structure is more complex than its model structure, you need to take care of one additional aspect to make the converters work &mdash; position mapping.

### The model-to-view position mapping

The downcast converters shown in the previous section will not work correctly yet. This is what the given model would look like, after being downcasted:

```
<infoBox infoBoxType="Info"> -> <div class="info-box info-box-info">
<paragraph> -> <p>
Foobar -> Foobar
</paragraph> -> </p>
<div class="info-box-title">Info</div>
<div class="info-box-content"></div>
</infoBox> -> </div>
```

This is not a correct view structure. The content of the model's `<infoBox>` element ended up directly inside the outer `<div>`. The `<infoBox>`'s content should be inside the `<div class="info-box-content">`.

You defined downcast conversion for `<infoBox>` itself, but you need to specify where its content should land in its view structure. By default, it is converted as direct children of `<div class="info-box">` (as shown in the above snippet) but it should go into `<div class="info-box-content">`. To achieve this, you need to register a callback for the {@link module:engine/conversion/mapper~Mapper#event:modelToViewPosition `Mapper#modelToViewPosition`} event, so positions inside the model `<infoBox>` element would map to positions inside the `<div class="info-box-content">` view element.

```
<infoBox infoBoxType="Info"> -> <div class="info-box info-box-info">
<div class="info-box-title">Info</div>
<div class="info-box-content">
<paragraph> -> <p>
Foobar -> Foobar
</paragraph> -> </p>
</div>
</infoBox> -> </div>
```

Such a mapping can be achieved by registering this callback to the {@link module:engine/conversion/mapper~Mapper#event:modelToViewPosition `Mapper#modelToViewPosition`} event:

```js
function createModelToViewPositionMapper( view ) {
return ( evt, data ) => {
const modelPosition = data.modelPosition;
const parent = modelPosition.parent;

// Only mapping of positions that are directly in
// the <infoBox> model element should be modified.
if ( !parent.is( 'element', 'infoBox' ) ) {
return;
}

// Get the mapped view element <div class="info-box">.
const viewElement = data.mapper.toViewElement( parent );

// Find the <div class="info-box-content"> 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 );
};
}

// Returns the <div class="info-box-content"> nested in the info box view structure.
function findContentViewElement( editingView, viewElement ) {
for ( const value of editingView.createRangeIn( viewElement ) ) {
if ( value.item.is( 'element', 'div' ) && value.item.hasClass( 'info-box-content' ) ) {
return value.item;
}
}
}
```

It needs to be plugged into the {@link module:engine/conversion/mapper~Mapper#event:modelToViewPosition `Mapper#modelToViewPosition`} event for both downcast pipelines:

```js
editor.editing.mapper.on( 'modelToViewPosition', createModelToViewPositionMapper( editor.editing.view ) );
editor.data.mapper.on( 'modelToViewPosition', createModelToViewPositionMapper( editor.editing.view ) );
```

<info-box>
**Note**: You do not need the reverse position mapping ({@link module:engine/conversion/mapper~Mapper#event:viewToModelPosition from the view to the model}) because the default view-to-model position mapping looks for the {@link module:engine/conversion/mapper~Mapper#findMappedViewAncestor mapped view ancestor} and maps the offset in respect to the model element.
</info-box>

### Updated plugin code

The updated `InfoBox` plugin that glues the event-based converters together:
Expand All @@ -298,6 +371,11 @@ class InfoBox {
.add( dispatcher => dispatcher.on( 'insert:infoBox', editingDowncastConverter ) );
editor.conversion.for( 'dataDowncast' )
.add( dispatcher => dispatcher.on( 'insert:infoBox', dataDowncastConverter ) );

// Model-to-view position mapper is needed since the model <infoBox> content needs to end up in the inner
// <div class="info-box-content">.
editor.editing.mapper.on( 'modelToViewPosition', createModelToViewPositionMapper( editor.editing.view ) );
editor.data.mapper.on( 'modelToViewPosition', createModelToViewPositionMapper( editor.editing.view ) );
}
}

Expand All @@ -312,4 +390,8 @@ function editingDowncastConverter() {
function dataDowncastConverter() {
// ...
}

function createModelToViewPositionMapper() {
// ...
}
```

0 comments on commit 9fd389a

Please sign in to comment.