diff --git a/src/conversion/downcasthelpers.js b/src/conversion/downcasthelpers.js index 97dc6e241..e8cc47832 100644 --- a/src/conversion/downcasthelpers.js +++ b/src/conversion/downcasthelpers.js @@ -444,10 +444,11 @@ export function createViewElementFromHighlightDescriptor( descriptor ) { * The converter automatically consumes the corresponding value from the consumables list and stops the event (see * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}). * - * modelDispatcher.on( 'attribute:bold', wrapItem( ( modelAttributeValue, viewWriter ) => { + * modelDispatcher.on( 'attribute:bold', wrap( ( modelAttributeValue, viewWriter ) => { * return viewWriter.createAttributeElement( 'strong' ); * } ); * + * @protected * @param {Function} elementCreator Function returning a view element that will be used for wrapping. * @returns {Function} Set/change attribute converter. */ diff --git a/tests/conversion/downcast-selection-converters.js b/tests/conversion/downcast-selection-converters.js index 719066914..fffc3e5d5 100644 --- a/tests/conversion/downcast-selection-converters.js +++ b/tests/conversion/downcast-selection-converters.js @@ -16,7 +16,7 @@ import { clearAttributes, } from '../../src/conversion/downcast-selection-converters'; -import DowncastHelpers, { insertText, wrap } from '../../src/conversion/downcasthelpers'; +import DowncastHelpers, { insertText } from '../../src/conversion/downcasthelpers'; import createViewRoot from '../view/_utils/createroot'; import { stringify as stringifyView } from '../../src/dev-utils/view'; @@ -45,10 +45,8 @@ describe( 'downcast-selection-converters', () => { dispatcher.on( 'insert:$text', insertText() ); - const strongCreator = ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'strong' ); - dispatcher.on( 'attribute:bold', wrap( strongCreator ) ); - downcastHelpers = new DowncastHelpers( dispatcher ); + downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } ); downcastHelpers.markerToHighlight( { model: 'marker', view: { classes: 'marker' }, converterPriority: 1 } ); // Default selection converters. diff --git a/tests/conversion/downcasthelpers.js b/tests/conversion/downcasthelpers.js index 5912139a3..8be542608 100644 --- a/tests/conversion/downcasthelpers.js +++ b/tests/conversion/downcasthelpers.js @@ -20,7 +20,7 @@ import ViewText from '../../src/view/text'; import log from '@ckeditor/ckeditor5-utils/src/log'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; -import DowncastHelpers, { createViewElementFromHighlightDescriptor, wrap } from '../../src/conversion/downcasthelpers'; +import DowncastHelpers, { createViewElementFromHighlightDescriptor } from '../../src/conversion/downcasthelpers'; import { stringify } from '../../src/dev-utils/view'; @@ -107,6 +107,10 @@ describe( 'DowncastHelpers', () => { } ); describe( 'attributeToElement()', () => { + beforeEach( () => { + downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } ); + } ); + it( 'should be chainable', () => { expect( downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } ) ).to.equal( downcastHelpers ); } ); @@ -264,6 +268,143 @@ describe( 'DowncastHelpers', () => { expect( viewToString( viewRoot ) ).to.equal( '
' ); } ); + + it( 'should convert insert/change/remove of attribute in model into wrapping element in a view', () => { + const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) ); + + downcastHelpers.attributeToElement( { + model: 'bold', + view: ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'b' ) + } ); + + model.change( writer => { + writer.insert( modelElement, modelRootStart ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + + model.change( writer => { + writer.removeAttribute( 'bold', writer.createRangeIn( modelElement ) ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + } ); + + it( 'should convert insert/remove of attribute in model with wrapping element generating function as a parameter', () => { + const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { style: 'bold' } ) ); + + downcastHelpers.attributeToElement( { + model: 'style', + view: ( modelAttributeValue, viewWriter ) => { + if ( modelAttributeValue == 'bold' ) { + return viewWriter.createAttributeElement( 'b' ); + } + } + } ); + + model.change( writer => { + writer.insert( modelElement, modelRootStart ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + + model.change( writer => { + writer.removeAttribute( 'style', writer.createRangeIn( modelElement ) ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + } ); + + it( 'should update range on re-wrapping attribute (#475)', () => { + const modelElement = new ModelElement( 'paragraph', null, [ + new ModelText( 'x' ), + new ModelText( 'foo', { link: 'http://foo.com' } ), + new ModelText( 'x' ) + ] ); + + downcastHelpers.attributeToElement( { + model: 'link', + view: ( modelAttributeValue, viewWriter ) => { + return viewWriter.createAttributeElement( 'a', { href: modelAttributeValue } ); + } + } ); + + model.change( writer => { + writer.insert( modelElement, modelRootStart ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

xfoox

' ); + + // Set new attribute on old link but also on non-linked characters. + model.change( writer => { + writer.setAttribute( 'link', 'http://foobar.com', writer.createRangeIn( modelElement ) ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

xfoox

' ); + } ); + + it( 'should support unicode', () => { + const modelElement = new ModelElement( 'paragraph', null, [ 'நி', new ModelText( 'லைக்', { bold: true } ), 'கு' ] ); + + downcastHelpers.attributeToElement( { + model: 'bold', + view: ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'b' ) + } ); + + model.change( writer => { + writer.insert( modelElement, modelRootStart ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

நிலைக்கு

' ); + + model.change( writer => { + writer.removeAttribute( 'bold', writer.createRangeIn( modelElement ) ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

நிலைக்கு

' ); + } ); + + it( 'should be possible to override ', () => { + const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) ); + + downcastHelpers.attributeToElement( { + model: 'bold', + view: ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'b' ) + } ); + downcastHelpers.attributeToElement( { + model: 'bold', + view: ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'strong' ), + converterPriority: 'high' + } ); + + model.change( writer => { + writer.insert( modelElement, modelRootStart ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + } ); + + it( 'should not convert and not consume if creator function returned null', () => { + sinon.spy( controller.downcastDispatcher, 'fire' ); + + const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { italic: true } ) ); + + downcastHelpers.attributeToElement( { + model: 'italic', + view: () => null + } ); + + const spy = sinon.spy(); + controller.downcastDispatcher.on( 'attribute:italic', spy ); + + model.change( writer => { + writer.insert( modelElement, modelRootStart ); + } ); + + expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); + expect( controller.downcastDispatcher.fire.calledWith( 'attribute:italic:$text' ) ).to.be.true; + expect( spy.called ).to.be.true; + } ); } ); describe( 'attributeToAttribute()', () => { @@ -1424,136 +1565,6 @@ describe( 'downcast-converters', () => { } ); } ); - describe( 'wrap', () => { - it( 'should convert insert/change/remove of attribute in model into wrapping element in a view', () => { - const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) ); - const creator = ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'b' ); - - dispatcher.on( 'attribute:bold', wrap( creator ) ); - - model.change( writer => { - writer.insert( modelElement, modelRootStart ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - - model.change( writer => { - writer.removeAttribute( 'bold', writer.createRangeIn( modelElement ) ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - } ); - - it( 'should convert insert/remove of attribute in model with wrapping element generating function as a parameter', () => { - const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { style: 'bold' } ) ); - - const elementGenerator = ( modelAttributeValue, viewWriter ) => { - if ( modelAttributeValue == 'bold' ) { - return viewWriter.createAttributeElement( 'b' ); - } - }; - - dispatcher.on( 'attribute:style', wrap( elementGenerator ) ); - - model.change( writer => { - writer.insert( modelElement, modelRootStart ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - - model.change( writer => { - writer.removeAttribute( 'style', writer.createRangeIn( modelElement ) ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - } ); - - it( 'should update range on re-wrapping attribute (#475)', () => { - const modelElement = new ModelElement( 'paragraph', null, [ - new ModelText( 'x' ), - new ModelText( 'foo', { link: 'http://foo.com' } ), - new ModelText( 'x' ) - ] ); - - const elementGenerator = ( modelAttributeValue, viewWriter ) => { - return viewWriter.createAttributeElement( 'a', { href: modelAttributeValue } ); - }; - - dispatcher.on( 'attribute:link', wrap( elementGenerator ) ); - - model.change( writer => { - writer.insert( modelElement, modelRootStart ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

xfoox

' ); - - // Set new attribute on old link but also on non-linked characters. - model.change( writer => { - writer.setAttribute( 'link', 'http://foobar.com', writer.createRangeIn( modelElement ) ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

xfoox

' ); - } ); - - it( 'should support unicode', () => { - const modelElement = new ModelElement( 'paragraph', null, [ 'நி', new ModelText( 'லைக்', { bold: true } ), 'கு' ] ); - const creator = ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'b' ); - - dispatcher.on( 'attribute:bold', wrap( creator ) ); - - model.change( writer => { - writer.insert( modelElement, modelRootStart ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

நிலைக்கு

' ); - - model.change( writer => { - writer.removeAttribute( 'bold', writer.createRangeIn( modelElement ) ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

நிலைக்கு

' ); - } ); - - it( 'should be possible to override wrap', () => { - const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) ); - - dispatcher.on( 'attribute:bold', wrap( ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'b' ) ) ); - - dispatcher.on( - 'attribute:bold', - wrap( ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'strong' ) ), - { priority: 'high' } - ); - - model.change( writer => { - writer.insert( modelElement, modelRootStart ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - } ); - - it( 'should not convert and not consume if creator function returned null', () => { - const elementGenerator = () => null; - - sinon.spy( dispatcher, 'fire' ); - - const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { italic: true } ) ); - - dispatcher.on( 'attribute:italic', wrap( elementGenerator ) ); - - const spy = sinon.spy(); - dispatcher.on( 'attribute:italic', spy ); - - model.change( writer => { - writer.insert( modelElement, modelRootStart ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); - expect( dispatcher.fire.calledWith( 'attribute:italic:$text' ) ).to.be.true; - expect( spy.called ).to.be.true; - } ); - } ); - // Remove converter is by default already added in `EditingController` instance. describe( 'remove', () => { it( 'should remove items from view accordingly to changes in model #1', () => {