Skip to content
This repository was archived by the owner on Jun 26, 2020. It is now read-only.

Commit fb6ab1a

Browse files
authored
Merge pull request #97 from ckeditor/t/8
Fix: Bare `<img>` (not wrapped with `<figure class="image">`) can now be pasted into the editor. Closes #8.
2 parents 5d7bef8 + 3f1db23 commit fb6ab1a

File tree

4 files changed

+131
-57
lines changed

4 files changed

+131
-57
lines changed

Diff for: src/image/converters.js

+21-28
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
* @module image/image/converters
88
*/
99

10-
import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
1110
import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position';
1211
import modelWriter from '@ckeditor/ckeditor5-engine/src/model/writer';
1312

@@ -20,52 +19,46 @@ import modelWriter from '@ckeditor/ckeditor5-engine/src/model/writer';
2019
*
2120
* <image src="..." alt="..."></image>
2221
*
22+
* The entire contents of `<figure>` except the first `<img>` is being converted as children
23+
* of the `<image>` model element.
24+
*
2325
* @returns {Function}
2426
*/
25-
export function viewToModelImage() {
27+
export function viewFigureToModel() {
2628
return ( evt, data, consumable, conversionApi ) => {
27-
const viewFigureElement = data.input;
28-
29-
// *** Step 1: Validate conversion.
30-
// Check if figure element can be consumed.
31-
if ( !consumable.test( viewFigureElement, { name: true, class: 'image' } ) ) {
29+
// Do not convert if this is not an "image figure".
30+
if ( !consumable.test( data.input, { name: true, class: 'image' } ) ) {
3231
return;
3332
}
3433

35-
// Check if image element can be converted in current context.
34+
// Do not convert if image cannot be placed in model at this context.
3635
if ( !conversionApi.schema.check( { name: 'image', inside: data.context, attributes: 'src' } ) ) {
3736
return;
3837
}
3938

40-
// Check if img element is placed inside figure element and can be consumed with `src` attribute.
41-
const viewImg = viewFigureElement.getChild( 0 );
39+
// Find an image element inside the figure element.
40+
const viewImage = Array.from( data.input.getChildren() ).find( viewChild => viewChild.is( 'img' ) );
4241

43-
if ( !viewImg || viewImg.name != 'img' || !consumable.test( viewImg, { name: true, attribute: 'src' } ) ) {
42+
// Do not convert if image element is absent, is missing src attribute or was already converted.
43+
if ( !viewImage || !viewImage.hasAttribute( 'src' ) || !consumable.test( viewImage, { name: true } ) ) {
4444
return;
4545
}
4646

47-
// *** Step2: Convert to model.
48-
consumable.consume( viewFigureElement, { name: true, class: 'image' } );
49-
consumable.consume( viewImg, { name: true, attribute: 'src' } );
47+
// Convert view image to model image.
48+
const modelImage = conversionApi.convertItem( viewImage, consumable, data );
5049

51-
// Create model element.
52-
const modelImage = new ModelElement( 'image', {
53-
src: viewImg.getAttribute( 'src' )
54-
} );
50+
// Convert rest of figure element's children, but in the context of model image, because those converted
51+
// children will be added as model image children.
52+
data.context.push( modelImage );
5553

56-
// Convert `alt` attribute if present.
57-
if ( consumable.consume( viewImg, { attribute: [ 'alt' ] } ) ) {
58-
modelImage.setAttribute( 'alt', viewImg.getAttribute( 'alt' ) );
59-
}
54+
const modelChildren = conversionApi.convertChildren( data.input, consumable, data );
6055

61-
// Convert children of converted view element and append them to `modelImage`.
62-
// TODO https://github.com/ckeditor/ckeditor5-engine/issues/736.
63-
data.context.push( modelImage );
64-
const modelChildren = conversionApi.convertChildren( viewFigureElement, consumable, data );
65-
const insertPosition = ModelPosition.createAt( modelImage, 'end' );
66-
modelWriter.insert( insertPosition, modelChildren );
6756
data.context.pop();
6857

58+
// Add converted children to model image.
59+
modelWriter.insert( ModelPosition.createAt( modelImage ), modelChildren );
60+
61+
// Set model image as conversion result.
6962
data.output = modelImage;
7063
};
7164
}

Diff for: src/image/imageengine.js

+15-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99

1010
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
1111
import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter';
12-
import { viewToModelImage, createImageAttributeConverter } from './converters';
12+
import buildViewConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildviewconverter';
13+
import { viewFigureToModel, createImageAttributeConverter } from './converters';
1314
import { toImageWidget } from './utils';
15+
import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
1416
import ViewContainerElement from '@ckeditor/ckeditor5-engine/src/view/containerelement';
1517
import ViewEmptyElement from '@ckeditor/ckeditor5-engine/src/view/emptyelement';
1618

@@ -52,8 +54,19 @@ export default class ImageEngine extends Plugin {
5254
createImageAttributeConverter( [ editing.modelToView, data.modelToView ], 'src' );
5355
createImageAttributeConverter( [ editing.modelToView, data.modelToView ], 'alt' );
5456

57+
// Build converter for view img element to model image element.
58+
buildViewConverter().for( data.viewToModel )
59+
.from( { name: 'img', attribute: { src: /./ } } )
60+
.toElement( ( viewImage ) => new ModelElement( 'image', { src: viewImage.getAttribute( 'src' ) } ) );
61+
62+
// Build converter for alt attribute.
63+
buildViewConverter().for( data.viewToModel )
64+
.from( { name: 'img', attribute: { alt: /./ } } )
65+
.consuming( { attribute: [ 'alt' ] } )
66+
.toAttribute( ( viewImage ) => ( { key: 'alt', value: viewImage.getAttribute( 'alt' ) } ) );
67+
5568
// Converter for figure element from view to model.
56-
data.viewToModel.on( 'element:figure', viewToModelImage() );
69+
data.viewToModel.on( 'element:figure', viewFigureToModel() );
5770
}
5871
}
5972

Diff for: tests/image/converters.js

+64-26
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
* For licensing, see LICENSE.md.
44
*/
55

6-
import { viewToModelImage, createImageAttributeConverter } from '../../src/image/converters';
6+
import { viewFigureToModel, createImageAttributeConverter } from '../../src/image/converters';
77
import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
88
import { createImageViewElement } from '../../src/image/imageengine';
99
import { toImageWidget } from '../../src/image/utils';
1010
import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter';
11+
import buildViewConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildviewconverter';
1112
import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
1213
import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
1314
import { setData as setModelData, getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
@@ -28,10 +29,6 @@ describe( 'Image converters', () => {
2829
schema.allow( { name: 'image', attributes: [ 'alt', 'src' ], inside: '$root' } );
2930
schema.objects.add( 'image' );
3031

31-
buildModelConverter().for( )
32-
.fromElement( 'image' )
33-
.toElement( () => toImageWidget( createImageViewElement() ) );
34-
3532
buildModelConverter().for( editor.editing.modelToView )
3633
.fromElement( 'image' )
3734
.toElement( () => toImageWidget( createImageViewElement() ) );
@@ -41,46 +38,87 @@ describe( 'Image converters', () => {
4138
} );
4239
} );
4340

44-
describe( 'viewToModelImage', () => {
45-
let dispatcher, schema;
41+
describe( 'viewFigureToModel', () => {
42+
function expectModel( model ) {
43+
expect( getModelData( document, { withoutSelection: true } ) ).to.equal( model );
44+
}
45+
46+
let dispatcher, schema, imgConverterCalled;
4647

4748
beforeEach( () => {
49+
imgConverterCalled = false;
50+
4851
schema = document.schema;
52+
schema.allow( { name: '$text', inside: 'image' } );
53+
4954
dispatcher = editor.data.viewToModel;
50-
dispatcher.on( 'element:figure', viewToModelImage() );
55+
dispatcher.on( 'element:figure', viewFigureToModel() );
56+
dispatcher.on( 'element:img', ( evt, data, consumable ) => {
57+
if ( consumable.consume( data.input, { name: true, attribute: 'src' } ) ) {
58+
data.output = new ModelElement( 'image', { src: data.input.getAttribute( 'src' ) } );
59+
60+
imgConverterCalled = true;
61+
}
62+
} );
5163
} );
5264

53-
it( 'should convert view figure element', () => {
54-
editor.setData( '<figure class="image"><img src="foo.png" alt="bar baz"></img></figure>' );
55-
expect( getModelData( document, { withoutSelection: true } ) ).to.equal( '<image alt="bar baz" src="foo.png"></image>' );
65+
it( 'should find img element among children and convert it using already defined converters', () => {
66+
editor.setData( '<figure class="image"><img src="foo.png" /></figure>' );
67+
68+
expectModel( '<image src="foo.png"></image>' );
69+
expect( imgConverterCalled ).to.be.true;
5670
} );
5771

58-
it( 'should convert without alt', () => {
59-
editor.setData( '<figure class="image"><img src="foo.png"></img></figure>' );
60-
expect( getModelData( document, { withoutSelection: true } ) ).to.equal( '<image src="foo.png"></image>' );
72+
it( 'should convert non-img children in image context and append them to model image element', () => {
73+
buildViewConverter().for( editor.data.viewToModel ).fromElement( 'foo' ).toElement( 'foo' );
74+
buildViewConverter().for( editor.data.viewToModel ).fromElement( 'bar' ).toElement( 'bar' );
75+
76+
schema.registerItem( 'foo' );
77+
schema.registerItem( 'bar' );
78+
79+
schema.allow( { name: 'foo', inside: 'image' } );
80+
81+
editor.setData( '<figure class="image">x<img src="foo.png" />y<foo></foo><bar></bar></figure>' );
82+
83+
// Element bar not converted because schema does not allow it.
84+
expectModel( '<image src="foo.png">xy<foo></foo></image>' );
6185
} );
6286

63-
it( 'should not convert if figure element is already consumed', () => {
87+
it( 'should be possible to overwrite', () => {
6488
dispatcher.on( 'element:figure', ( evt, data, consumable ) => {
65-
consumable.consume( data.input, { name: true, class: 'image' } );
89+
consumable.consume( data.input, { name: true } );
90+
consumable.consume( data.input.getChild( 0 ), { name: true } );
6691

67-
data.output = new ModelElement( 'not-image' );
92+
data.output = new ModelElement( 'myImage', { data: { src: data.input.getChild( 0 ).getAttribute( 'src' ) } } );
6893
}, { priority: 'high' } );
6994

70-
editor.setData( '<figure class="image"><img src="foo.png" alt="bar baz"></img></figure>' );
71-
expect( getModelData( document, { withoutSelection: true } ) ).to.equal( '<not-image></not-image>' );
95+
editor.setData( '<figure class="image"><img src="foo.png" />xyz</figure>' );
96+
97+
expectModel( '<myImage data="{"src":"foo.png"}"></myImage>' );
7298
} );
7399

74-
it( 'should not convert image if schema disallows it', () => {
75-
schema.disallow( { name: 'image', attributes: [ 'alt', 'src' ], inside: '$root' } );
100+
// Test exactly what figure converter does, which is putting it's children element to image element.
101+
// If this has not been done, it means that figure converter was not used.
102+
it( 'should not convert if figure do not have class="image" attribute', () => {
103+
editor.setData( '<figure><img src="foo.png" />xyz</figure>' );
76104

77-
editor.setData( '<figure class="image"><img src="foo.png"></img></figure>' );
78-
expect( getModelData( document, { withoutSelection: true } ) ).to.equal( '' );
105+
// Default image converter will be fired.
106+
expectModel( '<image src="foo.png"></image>' );
79107
} );
80108

81-
it( 'should not convert image if there is no img element', () => {
82-
editor.setData( '<figure class="image"></figure>' );
83-
expect( getModelData( document, { withoutSelection: true } ) ).to.equal( '' );
109+
it( 'should not convert if there is no img element among children', () => {
110+
editor.setData( '<figure class="image">xyz</figure>' );
111+
112+
// Figure converter outputs nothing and text is disallowed in root.
113+
expectModel( '' );
114+
} );
115+
116+
it( 'should not convert if img element was not converted', () => {
117+
// Image element missing src attribute.
118+
editor.setData( '<figure class="image"><img alt="abc" />xyz</figure>' );
119+
120+
// Figure converter outputs nothing and text is disallowed in root.
121+
expectModel( '' );
84122
} );
85123
} );
86124

Diff for: tests/image/imageengine.js

+31-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ describe( 'ImageEngine', () => {
5858
} );
5959

6060
it( 'should not convert if there is no image class', () => {
61-
editor.setData( '<figure><img src="foo.png" alt="alt text" /></figure>' );
61+
editor.setData( '<figure class="quote">My quote</figure>' );
6262

6363
expect( getModelData( document, { withoutSelection: true } ) )
6464
.to.equal( '' );
@@ -140,6 +140,36 @@ describe( 'ImageEngine', () => {
140140

141141
sinon.assert.calledOnce( conversionSpy );
142142
} );
143+
144+
it( 'should convert bare img element', () => {
145+
editor.setData( '<img src="foo.png" alt="alt text" />' );
146+
147+
expect( getModelData( document, { withoutSelection: true } ) )
148+
.to.equal( '<image alt="alt text" src="foo.png"></image>' );
149+
} );
150+
151+
it( 'should not convert alt attribute on non-img element', () => {
152+
const data = editor.data;
153+
const editing = editor.editing;
154+
155+
document.schema.registerItem( 'div', '$block' );
156+
document.schema.allow( { name: 'div', attributes: 'alt', inside: '$root' } );
157+
158+
buildModelConverter().for( data.modelToView, editing.modelToView ).fromElement( 'div' ).toElement( 'div' );
159+
buildViewConverter().for( data.viewToModel ).fromElement( 'div' ).toElement( 'div' );
160+
161+
editor.setData( '<div alt="foo"></div>' );
162+
163+
expect( getModelData( document, { withoutSelection: true } ) ).to.equal( '<div></div>' );
164+
} );
165+
166+
it( 'should handle figure with two images', () => {
167+
document.schema.allow( { name: '$text', inside: 'image' } );
168+
169+
editor.setData( '<figure class="image"><img src="foo.jpg" /><img src="bar.jpg" />abc</figure>' );
170+
171+
expect( getModelData( document, { withoutSelection: true } ) ).to.equal( '<image src="foo.jpg">abc</image>' );
172+
} );
143173
} );
144174
} );
145175

0 commit comments

Comments
 (0)