diff --git a/packages/ckeditor5-image/src/image/converters.ts b/packages/ckeditor5-image/src/image/converters.ts index e9ff6c94a9b..91f481f5005 100644 --- a/packages/ckeditor5-image/src/image/converters.ts +++ b/packages/ckeditor5-image/src/image/converters.ts @@ -229,13 +229,6 @@ export function downcastSourcesAttribute( imageUtils: ImageUtils ): ( dispatcher const attributeNewValue = data.attributeNewValue as null | Array; if ( attributeNewValue && attributeNewValue.length ) { - // Make sure does not break attribute elements, for instance in linked images. - const pictureElement = viewWriter.createContainerElement( 'picture', null, - attributeNewValue.map( sourceAttributes => { - return viewWriter.createEmptyElement( 'source', sourceAttributes ); - } ) - ); - // Collect all wrapping attribute elements. const attributeElements = []; let viewElement = imgElement.parent; @@ -249,8 +242,23 @@ export function downcastSourcesAttribute( imageUtils: ImageUtils ): ( dispatcher viewElement = parentElement; } - // Insert the picture and move img into it. - viewWriter.insert( viewWriter.createPositionBefore( imgElement ), pictureElement ); + const hasPictureElement = imgElement.parent!.is( 'element', 'picture' ); + + // Reuse existing element (ckeditor5#17192) or create a new one. + const pictureElement = hasPictureElement ? imgElement.parent : viewWriter.createContainerElement( 'picture', null ); + + if ( !hasPictureElement ) { + viewWriter.insert( viewWriter.createPositionBefore( imgElement ), pictureElement ); + } + + viewWriter.remove( viewWriter.createRangeIn( pictureElement ) ); + + viewWriter.insert( viewWriter.createPositionAt( pictureElement, 'end' ), + attributeNewValue.map( sourceAttributes => { + return viewWriter.createEmptyElement( 'source', sourceAttributes ); + } ) + ); + viewWriter.move( viewWriter.createRangeOn( imgElement ), viewWriter.createPositionAt( pictureElement, 'end' ) ); // Apply collected attribute elements over the new picture element. diff --git a/packages/ckeditor5-image/tests/pictureediting.js b/packages/ckeditor5-image/tests/pictureediting.js index 21ba7de4beb..80202ee49a9 100644 --- a/packages/ckeditor5-image/tests/pictureediting.js +++ b/packages/ckeditor5-image/tests/pictureediting.js @@ -1865,6 +1865,158 @@ describe( 'PictureEditing', () => { expect( editor.getData() ).to.equal( '

foobar

' ); } ); + + it( 'should downcast changed "sources" attribute on an existing picture element', () => { + editor.setData( + '
' + + '' + + '' + + '' + + '' + + '
Caption
' + + '
' + ); + + model.change( writer => { + writer.setAttribute( + 'sources', + [ + { + srcset: '/assets/sample2.png' + } + ], + modelDocument.getRoot().getChild( 0 ) + ); + } ); + + expect( getViewData( view, { withoutSelection: true } ) ).to.equal( + '
' + + '' + + '' + + '' + + '' + + '
' + + 'Caption' + + '
' + + '
' + ); + } ); + + it( 'should downcast changed "sources" attribute on an existing linked picture element', () => { + editor.setData( + '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '
Caption
' + + '
' + ); + + model.change( writer => { + writer.setAttribute( + 'sources', + [ + { + srcset: '/assets/sample2.png' + } + ], + modelDocument.getRoot().getChild( 0 ) + ); + } ); + + expect( getViewData( view, { withoutSelection: true } ) ).to.equal( + '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '
' + + 'Caption' + + '
' + + '
' + ); + } ); + + it( 'should keep existing picture element attributes when downcasting "sources" attribute', () => { + editor.model.schema.extend( 'imageBlock', { + allowAttributes: [ 'pictureClass' ] + } ); + + editor.conversion.for( 'upcast' ).add( dispatcher => { + dispatcher.on( 'element:picture', ( _evt, data, conversionApi ) => { + const viewItem = data.viewItem; + const modelElement = data.modelCursor.parent; + + conversionApi.writer.setAttribute( 'pictureClass', viewItem.getAttribute( 'class' ), modelElement ); + } ); + } ); + + editor.conversion.for( 'downcast' ).add( dispatcher => { + dispatcher.on( 'attribute:pictureClass:imageBlock', ( evt, data, conversionApi ) => { + const element = conversionApi.mapper.toViewElement( data.item ); + const pictureElement = element.getChild( 0 ); + + conversionApi.writer.setAttribute( 'class', data.attributeNewValue, pictureElement ); + } ); + } ); + + editor.setData( + '
' + + '' + + '' + + '' + + '' + + '
Caption
' + + '
' + ); + + model.change( writer => { + writer.setAttribute( + 'sources', + [ + { + srcset: '/assets/sample2.png' + } + ], + modelDocument.getRoot().getChild( 0 ) + ); + } ); + + expect( getViewData( view, { withoutSelection: true } ) ).to.equal( + '
' + + '' + + '' + + '' + + '' + + '
' + + 'Caption' + + '
' + + '
' + ); + } ); } ); } ); } );