Skip to content

Commit

Permalink
Merge pull request #7773 from ckeditor/t/7628
Browse files Browse the repository at this point in the history
Feature (engine): Options set in `Editor#getData()` and `DataController#get()` are now available in downcast conversion under `conversionApi.options` object. Closes #7628.
  • Loading branch information
scofalik authored Aug 10, 2020
2 parents 5e857fd + 790c50d commit 0a5d07e
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 10 deletions.
3 changes: 2 additions & 1 deletion packages/ckeditor5-core/src/editor/utils/dataapimixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ export default DataApiMixin;
* the right format for you.
*
* @method #getData
* @param {Object} [options]
* @param {Object} [options] Additional configuration for the retrieved data.
* Editor features may introduce more configuration options that can be set through this parameter.
* @param {String} [options.rootName='main'] Root name.
* @param {String} [options.trim='empty'] Whether returned data should be trimmed. This option is set to `'empty'` by default,
* which means that whenever editor content is considered empty, an empty string is returned. To turn off trimming
Expand Down
24 changes: 17 additions & 7 deletions packages/ckeditor5-engine/src/controller/datacontroller.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,15 +153,16 @@ export default class DataController {
* Returns the model's data converted by downcast dispatchers attached to {@link #downcastDispatcher} and
* formatted by the {@link #processor data processor}.
*
* @param {Object} [options]
* @param {Object} [options] Additional configuration for the retrieved data. `DataController` provides two optional
* properties: `rootName` and `trim`. Other properties of this object are specified by various editor features.
* @param {String} [options.rootName='main'] Root name.
* @param {String} [options.trim='empty'] Whether returned data should be trimmed. This option is set to `empty` by default,
* which means whenever editor content is considered empty, an empty string will be returned. To turn off trimming completely
* use `'none'`. In such cases exact content will be returned (for example `<p>&nbsp;</p>` for an empty editor).
* @returns {String} Output data.
*/
get( options ) {
const { rootName = 'main', trim = 'empty' } = options || {};
get( options = {} ) {
const { rootName = 'main', trim = 'empty' } = options;

if ( !this._checkIfRootsExists( [ rootName ] ) ) {
/**
Expand All @@ -184,7 +185,7 @@ export default class DataController {
return '';
}

return this.stringify( root );
return this.stringify( root, options );
}

/**
Expand All @@ -194,11 +195,12 @@ export default class DataController {
*
* @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} modelElementOrFragment
* Element whose content will be stringified.
* @param {Object} [options] Additional configuration passed to the conversion process.
* @returns {String} Output data.
*/
stringify( modelElementOrFragment ) {
stringify( modelElementOrFragment, options ) {
// Model -> view.
const viewDocumentFragment = this.toView( modelElementOrFragment );
const viewDocumentFragment = this.toView( modelElementOrFragment, options );

// View -> data.
return this.processor.toData( viewDocumentFragment );
Expand All @@ -212,9 +214,11 @@ export default class DataController {
*
* @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} modelElementOrFragment
* Element or document fragment whose content will be converted.
* @param {Object} [options] Additional configuration that will be available through
* {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi#options} during the conversion process.
* @returns {module:engine/view/documentfragment~DocumentFragment} Output view DocumentFragment.
*/
toView( modelElementOrFragment ) {
toView( modelElementOrFragment, options ) {
const viewDocument = this.viewDocument;
const viewWriter = this._viewWriter;

Expand All @@ -227,6 +231,9 @@ export default class DataController {

this.mapper.bindElements( modelElementOrFragment, viewDocumentFragment );

// Make additional options available during conversion process through `conversionApi`.
this.downcastDispatcher.conversionApi.options = options;

// We have no view controller and rendering to DOM in DataController so view.change() block is not used here.
this.downcastDispatcher.convertInsert( modelRange, viewWriter );

Expand All @@ -240,6 +247,9 @@ export default class DataController {
}
}

// Clean `conversionApi`.
delete this.downcastDispatcher.conversionApi.options;

return viewDocumentFragment;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import Consumable from './modelconsumable';
import Range from '../model/range';
import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
import mix from '@ckeditor/ckeditor5-utils/src/mix';
import { extend } from 'lodash-es';

/**
* Downcast dispatcher is a central point of downcasting (conversion from the model to the view), which is a process of reacting to changes
Expand Down Expand Up @@ -115,7 +114,7 @@ export default class DowncastDispatcher {
*
* @member {module:engine/conversion/downcastdispatcher~DowncastConversionApi}
*/
this.conversionApi = extend( { dispatcher: this }, conversionApi );
this.conversionApi = Object.assign( { dispatcher: this }, conversionApi );
}

/**
Expand Down Expand Up @@ -669,3 +668,9 @@ function shouldMarkerChangeBeConverted( modelPosition, marker, mapper ) {
*
* @member {module:engine/view/downcastwriter~DowncastWriter} #writer
*/

/**
* An object with an additional configuration which can be used during conversion process. Available only for data downcast conversion.
*
* @member {Object} #options
*/
124 changes: 124 additions & 0 deletions packages/ckeditor5-engine/tests/controller/datacontroller.js
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,82 @@ describe( 'DataController', () => {
data.get( { rootName: 'nonexistent' } );
}, /datacontroller-get-non-existent-root:/ );
} );

it( 'should allow to provide additional options for retrieving data - insert conversion', () => {
schema.register( 'paragraph', { inheritAllFrom: '$block' } );

data.downcastDispatcher.on( 'insert:paragraph', ( evt, data, conversionApi ) => {
conversionApi.consumable.consume( data.item, 'insert' );

const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
const viewElement = conversionApi.writer.createContainerElement( 'p', {
attribute: conversionApi.options.attributeValue
} );

conversionApi.mapper.bindElements( data.item, viewElement );
conversionApi.writer.insert( viewPosition, viewElement );
}, { priority: 'high' } );

setData( model, '<paragraph>foo</paragraph>' );

expect( data.get( { attributeValue: 'foo' } ) ).to.equal( '<p attribute="foo">foo</p>' );
expect( data.get( { attributeValue: 'bar' } ) ).to.equal( '<p attribute="bar">foo</p>' );
} );

it( 'should allow to provide additional options for retrieving data - attribute conversion', () => {
schema.register( 'paragraph', { inheritAllFrom: '$block', allowAttributes: [ 'foo' ] } );
downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );

data.downcastDispatcher.on( 'attribute:foo', ( evt, data, conversionApi ) => {
if ( data.attributeNewValue === conversionApi.options.skipAttribute ) {
return;
}

const viewRange = conversionApi.mapper.toViewRange( data.range );
const viewElement = conversionApi.writer.createAttributeElement( data.attributeNewValue );

conversionApi.writer.wrap( viewRange, viewElement );
} );

setData( model, '<paragraph>f<$text foo="a">o</$text>ob<$text foo="b">a</$text>r</paragraph>' );

expect( data.get() ).to.equal( '<p>f<a>o</a>ob<b>a</b>r</p>' );
expect( data.get( { skipAttribute: 'a' } ) ).to.equal( '<p>foob<b>a</b>r</p>' );
expect( data.get( { skipAttribute: 'b' } ) ).to.equal( '<p>f<a>o</a>obar</p>' );
} );

it( 'should allow to provide additional options for retrieving data - addMarker conversion', () => {
schema.register( 'paragraph', { inheritAllFrom: '$block' } );
downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );

data.downcastDispatcher.on( 'addMarker', ( evt, data, conversionApi ) => {
if ( conversionApi.options.skipMarker ) {
return;
}

const viewElement = conversionApi.writer.createAttributeElement( 'marker' );
const viewRange = conversionApi.mapper.toViewRange( data.markerRange );

conversionApi.writer.wrap( viewRange, viewElement );
} );

setData( model, '<paragraph>foo</paragraph>' );

const root = model.document.getRoot();

model.change( writer => {
const start = writer.createPositionFromPath( root, [ 0, 1 ] );
const end = writer.createPositionFromPath( root, [ 0, 2 ] );

writer.addMarker( 'marker', {
range: writer.createRange( start, end ),
usingOperation: false
} );
} );

expect( data.get( { skipMarker: false } ) ).to.equal( '<p>f<marker>o</marker>o</p>' );
expect( data.get( { skipMarker: true } ) ).to.equal( '<p>foo</p>' );
} );
} );

describe( 'stringify()', () => {
Expand All @@ -478,6 +554,24 @@ describe( 'DataController', () => {

expect( data.stringify( modelDocumentFragment ) ).to.equal( '<p>foo</p><p>bar</p>' );
} );

it( 'should allow to provide additional options to the conversion process', () => {
const spy = sinon.spy();

data.downcastDispatcher.on( 'insert:paragraph', ( evt, data, conversionApi ) => {
spy( conversionApi.options );
}, { priority: 'high' } );

const modelDocumentFragment = parseModel( '<paragraph>foo</paragraph><paragraph>bar</paragraph>', schema );

const options = { foo: 'bar' };

data.stringify( modelDocumentFragment );
expect( spy.lastCall.args[ 0 ] ).to.not.equal( options );

data.stringify( modelDocumentFragment, options );
expect( spy.lastCall.args[ 0 ] ).to.equal( options );
} );
} );

describe( 'toView()', () => {
Expand Down Expand Up @@ -590,6 +684,36 @@ describe( 'DataController', () => {
expect( mappedViewRange.end.nodeBefore ).to.equal( firstViewElement );
expect( mappedViewRange.end.nodeAfter ).to.equal( viewDocumentFragment.getChild( 1 ) );
} );

it( 'should allow to provide additional options to the conversion process', () => {
const root = model.document.getRoot();
const spy = sinon.spy();

data.downcastDispatcher.on( 'insert:paragraph', ( evt, data, conversionApi ) => {
spy( conversionApi.options );
}, { priority: 'high' } );

data.downcastDispatcher.on( 'addMarker:marker', ( evt, data, conversionApi ) => {
spy( conversionApi.options );
}, { priority: 'high' } );

setData( model, '<paragraph>foo</paragraph>' );

model.change( writer => {
writer.addMarker( 'marker', {
range: model.createRange( model.createPositionFromPath( root, [ 0, 1 ] ) ),
usingOperation: false
} );
} );

const options = { foo: 'bar' };

data.toView( root, options );

sinon.assert.calledTwice( spy );
expect( spy.firstCall.args[ 0 ] ).to.equal( options );
expect( spy.lastCall.args[ 0 ] ).to.equal( options );
} );
} );

describe( 'destroy()', () => {
Expand Down

0 comments on commit 0a5d07e

Please sign in to comment.