diff --git a/packages/ckeditor5-core/src/editor/utils/dataapimixin.js b/packages/ckeditor5-core/src/editor/utils/dataapimixin.js index 057d8bb8883..be18fb1d6fd 100644 --- a/packages/ckeditor5-core/src/editor/utils/dataapimixin.js +++ b/packages/ckeditor5-core/src/editor/utils/dataapimixin.js @@ -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 diff --git a/packages/ckeditor5-engine/src/controller/datacontroller.js b/packages/ckeditor5-engine/src/controller/datacontroller.js index 444cd2019a5..8f536d88c21 100644 --- a/packages/ckeditor5-engine/src/controller/datacontroller.js +++ b/packages/ckeditor5-engine/src/controller/datacontroller.js @@ -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 `
` 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 ] ) ) { /** @@ -184,7 +185,7 @@ export default class DataController { return ''; } - return this.stringify( root ); + return this.stringify( root, options ); } /** @@ -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 ); @@ -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; @@ -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 ); @@ -240,6 +247,9 @@ export default class DataController { } } + // Clean `conversionApi`. + delete this.downcastDispatcher.conversionApi.options; + return viewDocumentFragment; } diff --git a/packages/ckeditor5-engine/src/conversion/downcastdispatcher.js b/packages/ckeditor5-engine/src/conversion/downcastdispatcher.js index aa2ab403506..1fdc18a22f4 100644 --- a/packages/ckeditor5-engine/src/conversion/downcastdispatcher.js +++ b/packages/ckeditor5-engine/src/conversion/downcastdispatcher.js @@ -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 @@ -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 ); } /** @@ -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 + */ diff --git a/packages/ckeditor5-engine/tests/controller/datacontroller.js b/packages/ckeditor5-engine/tests/controller/datacontroller.js index 043d88221a0..94647822dab 100644 --- a/packages/ckeditor5-engine/tests/controller/datacontroller.js +++ b/packages/ckeditor5-engine/tests/controller/datacontroller.js @@ -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, '
foo
' ); + expect( data.get( { attributeValue: 'bar' } ) ).to.equal( 'foo
' ); + } ); + + 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, 'foobar
' ); + expect( data.get( { skipAttribute: 'a' } ) ).to.equal( 'foobar
' ); + expect( data.get( { skipAttribute: 'b' } ) ).to.equal( 'foobar
' ); + } ); + + 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, 'f
foo
' ); + } ); } ); describe( 'stringify()', () => { @@ -478,6 +554,24 @@ describe( 'DataController', () => { expect( data.stringify( modelDocumentFragment ) ).to.equal( 'foo
bar
' ); } ); + + 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( '