Skip to content

Commit

Permalink
Merge pull request #7652 from ckeditor/i/7336
Browse files Browse the repository at this point in the history
Feature (engine): Introduced new upcast `ConversionApi` helper methods - `conversionApi.safeInsert()` and `conversionApi.updateConversionResult()`. New methods are intended to simplify writing event based element-to-element converters. Closes #7336.

MAJOR BREAKING CHANGE (engine): The `config.view` parameter for upcast element-to-element conversion helpers configurations is again mandatory. You can retain previous "catch-all" behavior for upcast converter using `config.view = /[\s\S]+/`.
  • Loading branch information
niegowski authored Aug 5, 2020
2 parents 7984a14 + 1f0b6db commit 8d84af1
Show file tree
Hide file tree
Showing 20 changed files with 1,047 additions and 279 deletions.
40 changes: 3 additions & 37 deletions packages/ckeditor5-code-block/src/converters.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,48 +193,14 @@ export function dataViewToModelCodeBlockInsertion( editingView, languageDefs ) {

writer.append( fragment, codeBlock );

// Let's see if the codeBlock can be inserted the current modelCursor.
const splitResult = conversionApi.splitToAllowedParent( codeBlock, data.modelCursor );

// When there is no split result it means that we can't insert element to model tree,
// so let's skip it.
if ( !splitResult ) {
// Let's try to insert code block.
if ( !conversionApi.safeInsert( codeBlock, data.modelCursor ) ) {
return;
}

// Insert element on allowed position.
writer.insert( codeBlock, splitResult.position );

consumable.consume( viewItem, { name: true } );
consumable.consume( viewChild, { name: true } );

const parts = conversionApi.getSplitParts( codeBlock );

// Set conversion result range.
data.modelRange = writer.createRange(
conversionApi.writer.createPositionBefore( codeBlock ),
conversionApi.writer.createPositionAfter( parts[ parts.length - 1 ] )
);

// If we had to split parent to insert our element then we want to continue conversion inside
// the split parent.
//
// before split:
//
// <allowed><notAllowed>[]</notAllowed></allowed>
//
// after split:
//
// <allowed>
// <notAllowed></notAllowed>
// <converted></converted>
// <notAllowed>[]</notAllowed>
// </allowed>
if ( splitResult.cursorParent ) {
data.modelCursor = writer.createPositionAt( splitResult.cursorParent, 0 );
} else {
// Otherwise just continue after the inserted element.
data.modelCursor = data.modelRange.end;
}
conversionApi.updateConversionResult( codeBlock, data );
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

/* globals window */

import ClassicEditor from '@ckeditor/ckeditor5-build-classic/src/ckeditor';

import { toWidget, toWidgetEditable } from '@ckeditor/ckeditor5-widget/src/utils';

window.ClassicEditor = ClassicEditor;
window.toWidget = toWidget;
window.toWidgetEditable = toWidgetEditable;
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<style>
.info-box {
border: 1px solid hsl(0, 0%, 80%);
padding: 1em;
background: hsl(0, 0%, 45%);
}

.info-box-warning {
background: hsl(64, 74%, 85%);
}

.info-box-info {
background: hsl(205, 100%, 90%);
}

.info-box-title {
margin-bottom: 1em;
font-weight: bold;
color: inherit;
}

.info-box-content {
padding: 0 1em;
background: hsl(0, 0%, 100%);
}
</style>

<div id="editor-custom-element-converter">
<p>Info:</p>
<div class="info-box info-box-info">
<div class="info-box-title">Info</div>
<div class="info-box-content">
<p>Editable content of the <strong>info box</strong>.</p>
</div>
</div>
<p>Warning:</p>
<div class="info-box info-box-warning">
<div class="info-box-title">Warning</div>
<div class="info-box-content">
<p>Editable content of the <strong>info box</strong>.</p>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/**
* @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

/* globals ClassicEditor, toWidget, toWidgetEditable, console, window, document */

import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';

class InfoBox {
constructor( editor ) {
// Schema definition
editor.model.schema.register( 'infoBox', {
allowWhere: '$block',
allowContentOf: '$root',
isObject: true,
allowAttributes: [ 'infoBoxType' ]
} );

// Upcast converter.
editor.conversion.for( 'upcast' )
.add( dispatcher => dispatcher.on( 'element:div', upcastConverter ) );

// The downcast conversion must be split as we need a widget in the editing pipeline.
editor.conversion.for( 'editingDowncast' )
.add( dispatcher => dispatcher.on( 'insert:infoBox', editingDowncastConverter ) );
editor.conversion.for( 'dataDowncast' )
.add( dispatcher => dispatcher.on( 'insert:infoBox', dataDowncastConverter ) );
}
}

function upcastConverter( event, data, conversionApi ) {
const viewInfoBox = data.viewItem;

// Detect that view element is an info-box div.
// Otherwise, it should be handled by another converter.
if ( !viewInfoBox.hasClass( 'info-box' ) ) {
return;
}

// Create a model structure.
const modelElement = conversionApi.writer.createElement( 'infoBox', {
infoBoxType: getTypeFromViewElement( viewInfoBox )
} );

// Try to safely insert element - if it returns false the element can't be safely inserted
// into the content, and the conversion process must stop.
if ( !conversionApi.safeInsert( modelElement, data.modelCursor ) ) {
return;
}

// Mark 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.
const viewInfoBoxTitle = viewInfoBox.getChild( 0 );
const viewInfoBoxContent = viewInfoBox.getChild( 1 );

// Mark info-box inner elements 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.
conversionApi.convertChildren( viewInfoBoxContent, modelElement );

// Conversion requires updating result data structure properly.
conversionApi.updateConversionResult( modelElement, data );
}

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' } );
infoBoxContent = toWidgetEditable( infoBoxContent, conversionApi.writer );

insertViewElements( data, conversionApi, infoBox, infoBoxTitle, infoBoxContent );
}

function dataDowncastConverter( event, data, conversionApi ) {
const { infoBox, infoBoxContent, infoBoxTitle } = createViewElements( data, conversionApi );

insertViewElements( data, conversionApi, infoBox, infoBoxTitle, infoBoxContent );
}

function createViewElements( data, conversionApi ) {
const type = data.item.getAttribute( 'infoBoxType' );

const infoBox = conversionApi.writer.createContainerElement( 'div', {
class: `info-box info-box-${ type.toLowerCase() }`
} );
const infoBoxContent = conversionApi.writer.createEditableElement( 'div', {
class: 'info-box-content'
} );

const infoBoxTitle = conversionApi.writer.createUIElement( 'div',
{ class: 'info-box-title' },
function( domDocument ) {
const domElement = this.toDomElement( domDocument );

domElement.innerText = type;

return domElement;
} );

return { infoBox, infoBoxContent, infoBoxTitle };
}

function insertViewElements( data, conversionApi, infoBox, infoBoxTitle, infoBoxContent ) {
conversionApi.consumable.consume( data.item, 'insert' );

conversionApi.writer.insert(
conversionApi.writer.createPositionAt( infoBox, 0 ),
infoBoxTitle
);
conversionApi.writer.insert(
conversionApi.writer.createPositionAt( infoBox, 1 ),
infoBoxContent
);

conversionApi.mapper.bindElements( data.item, infoBox );
conversionApi.mapper.bindElements( data.item, infoBoxContent );

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

ClassicEditor
.create( document.querySelector( '#editor-custom-element-converter' ), {
cloudServices: CS_CONFIG,
extraPlugins: [ InfoBox ],
image: {
toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ]
},
table: {
contentToolbar: [
'tableColumn',
'tableRow',
'mergeTableCells'
]
},
toolbar: {
viewportTopOffset: window.getViewportTopOffsetConfig()
}
} )
.then( editor => {
window.editor = editor;
} )
.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 @@ -8,11 +8,11 @@ order: 10
# For now, due to lack of content, it is called "advanced concepts".
---

# Advanced conversion concepts
# Advanced conversion concepts &mdash; attributes

This guide extends the {@link framework/guides/architecture/editing-engine introduction to CKEditor 5 editing engine architecture}. Therefore, we highly recommend reading the former guide first.

In this guide we will dive deeper into some of the conversion concepts.
In this guide we will dive deeper into some of the conversion concepts related to model attributes.

## Inline and block content

Expand Down
Loading

0 comments on commit 8d84af1

Please sign in to comment.