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

Commit dec9c28

Browse files
authored
Merge pull request #1141 from ckeditor/t/1139b
Fix: View and model nodes will now be removed from their old parents when they are added to a new parent to prevent having same node on multiple elements' children lists. Closes #1139. BREAKING CHANGE: View and model nodes are now automatically removed from their old parents when they are inserted into new elements. This is important e.g. if you iterate through element's children and they are moved during that iteration. In that case, it's safest to cache the element's children in an array.
2 parents 1be7ed1 + 7a90bc2 commit dec9c28

File tree

9 files changed

+32
-48
lines changed

9 files changed

+32
-48
lines changed

src/dev-utils/model.js

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -126,22 +126,8 @@ export function setData( document, data, options = {} ) {
126126
const ranges = [];
127127

128128
for ( const range of selection.getRanges() ) {
129-
let start, end;
130-
131-
// Each range returned from `parse()` method has its root placed in DocumentFragment.
132-
// Here we convert each range to have its root re-calculated properly and be placed inside
133-
// model document root.
134-
if ( range.start.parent.is( 'documentFragment' ) ) {
135-
start = ModelPosition.createFromParentAndOffset( modelRoot, range.start.offset );
136-
} else {
137-
start = ModelPosition.createFromParentAndOffset( range.start.parent, range.start.offset );
138-
}
139-
140-
if ( range.end.parent.is( 'documentFragment' ) ) {
141-
end = ModelPosition.createFromParentAndOffset( modelRoot, range.end.offset );
142-
} else {
143-
end = ModelPosition.createFromParentAndOffset( range.end.parent, range.end.offset );
144-
}
129+
const start = new ModelPosition( modelRoot, range.start.path );
130+
const end = new ModelPosition( modelRoot, range.end.path );
145131

146132
ranges.push( new ModelRange( start, end ) );
147133
}

src/dev-utils/view.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -852,7 +852,9 @@ function _convertViewElements( rootNode ) {
852852
const convertedElement = rootNode.is( 'documentFragment' ) ? new ViewDocumentFragment() : _convertElement( rootNode );
853853

854854
// Convert all child nodes.
855-
for ( const child of rootNode.getChildren() ) {
855+
// Cache the nodes in array. Otherwise, we would skip some nodes because during iteration we move nodes
856+
// from `rootNode` to `convertedElement`. This would interfere with iteration.
857+
for ( const child of [ ...rootNode.getChildren() ] ) {
856858
if ( convertedElement.is( 'emptyElement' ) ) {
857859
throw new Error( 'Parse error - cannot parse inside EmptyElement.' );
858860
}

src/model/documentfragment.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,11 @@ export default class DocumentFragment {
235235
nodes = normalize( nodes );
236236

237237
for ( const node of nodes ) {
238+
// If node that is being added to this element is already inside another element, first remove it from the old parent.
239+
if ( node.parent !== null ) {
240+
node.remove();
241+
}
242+
238243
node.parent = this;
239244
}
240245

src/model/element.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,11 @@ export default class Element extends Node {
202202
nodes = normalize( nodes );
203203

204204
for ( const node of nodes ) {
205+
// If node that is being added to this element is already inside another element, first remove it from the old parent.
206+
if ( node.parent !== null ) {
207+
node.remove();
208+
}
209+
205210
node.parent = this;
206211
}
207212

src/view/documentfragment.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,11 @@ export default class DocumentFragment {
150150
nodes = normalize( nodes );
151151

152152
for ( const node of nodes ) {
153+
// If node that is being added to this element is already inside another element, first remove it from the old parent.
154+
if ( node.parent !== null ) {
155+
node.remove();
156+
}
157+
153158
node.parent = this;
154159

155160
this._children.splice( index, 0, node );

src/view/element.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,11 @@ export default class Element extends Node {
355355
nodes = normalize( nodes );
356356

357357
for ( const node of nodes ) {
358+
// If node that is being added to this element is already inside another element, first remove it from the old parent.
359+
if ( node.parent !== null ) {
360+
node.remove();
361+
}
362+
358363
node.parent = this;
359364

360365
this._children.splice( index, 0, node );

tests/conversion/advanced-converters.js

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -287,8 +287,6 @@ describe( 'advanced-converters', () => {
287287
it( 'should convert view image to model', () => {
288288
const viewElement = new ViewContainerElement( 'img', { src: 'bar.jpg', title: 'bar' } );
289289
const modelElement = viewDispatcher.convert( viewElement );
290-
// Attaching to tree so tree walker works fine in `modelToString`.
291-
modelRoot.appendChildren( modelElement );
292290

293291
expect( modelToString( modelElement ) ).to.equal( '<image src="bar.jpg" title="bar"></image>' );
294292
} );
@@ -303,8 +301,6 @@ describe( 'advanced-converters', () => {
303301
]
304302
);
305303
const modelElement = viewDispatcher.convert( viewElement );
306-
// Attaching to tree so tree walker works fine in `modelToString`.
307-
modelRoot.appendChildren( modelElement );
308304

309305
expect( modelToString( modelElement ) ).to.equal( '<image src="bar.jpg" title="bar"><caption>foobar</caption></image>' );
310306
} );
@@ -595,7 +591,6 @@ describe( 'advanced-converters', () => {
595591
);
596592

597593
const modelElement = viewDispatcher.convert( viewElement );
598-
modelRoot.appendChildren( modelElement );
599594

600595
expect( modelToString( modelElement ) ).to.equal( '<quote linkHref="foo.html" linkTitle="Foo source">foo</quote>' );
601596
} );
@@ -761,7 +756,6 @@ describe( 'advanced-converters', () => {
761756
] );
762757

763758
const modelElement = viewDispatcher.convert( viewElement );
764-
modelRoot.appendChildren( modelElement );
765759

766760
expect( modelToString( modelElement ) ).to.equal(
767761
'<table cellpadding="5" cellspacing="5">' +

tests/conversion/buildviewconverter.js

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import buildViewConverter from '../../src/conversion/buildviewconverter';
77

88
import ModelSchema from '../../src/model/schema';
99
import ModelDocumentFragment from '../../src/model/documentfragment';
10-
import ModelDocument from '../../src/model/document';
1110
import ModelElement from '../../src/model/element';
1211
import ModelTextProxy from '../../src/model/textproxy';
1312
import ModelRange from '../../src/model/range';
@@ -64,7 +63,7 @@ const textAttributes = [ undefined, 'linkHref', 'linkTitle', 'bold', 'italic', '
6463
const pAttributes = [ undefined, 'class', 'important', 'theme', 'decorated', 'size' ];
6564

6665
describe( 'View converter builder', () => {
67-
let dispatcher, modelDoc, modelRoot, schema, objWithContext;
66+
let dispatcher, schema, objWithContext;
6867

6968
beforeEach( () => {
7069
// `additionalData` parameter for `.convert` calls.
@@ -91,16 +90,12 @@ describe( 'View converter builder', () => {
9190

9291
dispatcher = new ViewConversionDispatcher( { schema } );
9392
dispatcher.on( 'text', convertText() );
94-
95-
modelDoc = new ModelDocument();
96-
modelRoot = modelDoc.createRoot();
9793
} );
9894

9995
it( 'should convert from view element to model element', () => {
10096
buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
10197

10298
const conversionResult = dispatcher.convert( new ViewContainerElement( 'p', null, new ViewText( 'foo' ) ), objWithContext );
103-
modelRoot.appendChildren( conversionResult );
10499

105100
expect( modelToString( conversionResult ) ).to.equal( '<paragraph>foo</paragraph>' );
106101
} );
@@ -111,7 +106,6 @@ describe( 'View converter builder', () => {
111106
.toElement( viewElement => new ModelElement( 'image', { src: viewElement.getAttribute( 'src' ) } ) );
112107

113108
const conversionResult = dispatcher.convert( new ViewContainerElement( 'img', { src: 'foo.jpg' } ), objWithContext );
114-
modelRoot.appendChildren( conversionResult );
115109

116110
expect( modelToString( conversionResult ) ).to.equal( '<image src="foo.jpg"></image>' );
117111
} );
@@ -122,10 +116,9 @@ describe( 'View converter builder', () => {
122116
const conversionResult = dispatcher.convert(
123117
new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), objWithContext
124118
);
125-
modelRoot.appendChildren( conversionResult );
126119

127120
// Have to check root because result is a ModelText.
128-
expect( modelToString( modelRoot ) ).to.equal( '<$root><$text bold="true">foo</$text></$root>' );
121+
expect( modelToString( conversionResult ) ).to.equal( '<$text bold="true">foo</$text>' );
129122
} );
130123

131124
it( 'should convert from view element to model attributes using creator function', () => {
@@ -136,10 +129,9 @@ describe( 'View converter builder', () => {
136129
const conversionResult = dispatcher.convert(
137130
new ViewAttributeElement( 'a', { href: 'foo.html' }, new ViewText( 'foo' ) ), objWithContext
138131
);
139-
modelRoot.appendChildren( conversionResult );
140132

141133
// Have to check root because result is a ModelText.
142-
expect( modelToString( modelRoot ) ).to.equal( '<$root><$text linkHref="foo.html">foo</$text></$root>' );
134+
expect( modelToString( conversionResult ) ).to.equal( '<$text linkHref="foo.html">foo</$text>' );
143135
} );
144136

145137
it( 'should convert from view attribute to model attribute', () => {
@@ -152,7 +144,6 @@ describe( 'View converter builder', () => {
152144
const conversionResult = dispatcher.convert(
153145
new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), objWithContext
154146
);
155-
modelRoot.appendChildren( conversionResult );
156147

157148
expect( modelToString( conversionResult ) ).to.equal( '<paragraph class="myClass">foo</paragraph>' );
158149
} );
@@ -200,7 +191,6 @@ describe( 'View converter builder', () => {
200191
] );
201192

202193
const conversionResult = dispatcher.convert( viewElement, objWithContext );
203-
modelRoot.appendChildren( conversionResult );
204194

205195
expect( modelToString( conversionResult ) ).to.equal( '<paragraph><$text bold="true">aaabbbcccddd</$text></paragraph>' );
206196
} );
@@ -337,7 +327,7 @@ describe( 'View converter builder', () => {
337327
result = dispatcher.convert(
338328
new ViewContainerElement( 'span', { class: 'megatron' }, new ViewText( 'foo' ) ), objWithContext
339329
);
340-
modelRoot.appendChildren( result );
330+
341331
expect( modelToString( result ) ).to.equal( '<span>foo</span>' );
342332

343333
// Almost a megatron. Missing a head.
@@ -346,7 +336,6 @@ describe( 'View converter builder', () => {
346336
objWithContext
347337
);
348338

349-
modelRoot.appendChildren( result );
350339
expect( modelToString( result ) ).to.equal( '<span>foo</span>' );
351340

352341
// This would be a megatron but is a paragraph.
@@ -359,7 +348,6 @@ describe( 'View converter builder', () => {
359348
objWithContext
360349
);
361350

362-
modelRoot.appendChildren( result );
363351
expect( modelToString( result ) ).to.equal( '<paragraph>foo</paragraph>' );
364352

365353
// At last we have a megatron!
@@ -372,7 +360,6 @@ describe( 'View converter builder', () => {
372360
objWithContext
373361
);
374362

375-
modelRoot.appendChildren( result );
376363
expect( modelToString( result ) ).to.equal( '<MEGATRON>foo</MEGATRON>' );
377364
} );
378365

@@ -392,7 +379,6 @@ describe( 'View converter builder', () => {
392379

393380
const conversionResult = dispatcher.convert( viewElement, objWithContext );
394381

395-
modelRoot.appendChildren( conversionResult );
396382
expect( modelToString( conversionResult ) ).to.equal( '<span transformer="megatron">foo</span>' );
397383
} );
398384

@@ -415,7 +401,6 @@ describe( 'View converter builder', () => {
415401
const conversionResult = dispatcher.convert(
416402
new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), objWithContext
417403
);
418-
modelRoot.appendChildren( conversionResult );
419404

420405
// Element converter was fired first even though attribute converter was added first.
421406
expect( modelToString( conversionResult ) ).to.equal( '<paragraph class="myClass">foo</paragraph>' );
@@ -432,7 +417,7 @@ describe( 'View converter builder', () => {
432417
result = dispatcher.convert(
433418
new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), objWithContext
434419
);
435-
modelRoot.appendChildren( result );
420+
436421
expect( modelToString( result ) ).to.equal( '<paragraph class="myClass">foo</paragraph>' );
437422

438423
buildViewConverter().for( dispatcher )
@@ -442,7 +427,7 @@ describe( 'View converter builder', () => {
442427
result = dispatcher.convert(
443428
new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), objWithContext
444429
);
445-
modelRoot.appendChildren( result );
430+
446431
expect( modelToString( result ) ).to.equal( '<customP>foo</customP>' );
447432
} );
448433

@@ -461,9 +446,7 @@ describe( 'View converter builder', () => {
461446
.toAttribute( 'size', 'small' );
462447

463448
const viewElement = new ViewContainerElement( 'p', { class: 'decorated small' }, new ViewText( 'foo' ) );
464-
465449
const conversionResult = dispatcher.convert( viewElement, objWithContext );
466-
modelRoot.appendChildren( conversionResult );
467450

468451
// P element and it's children got converted by the converter (1) and the converter (1) got fired
469452
// because P name was not consumed in converter (2). Converter (3) could consume class="small" because
@@ -487,7 +470,6 @@ describe( 'View converter builder', () => {
487470
] );
488471

489472
const conversionResult = dispatcher.convert( viewStructure, objWithContext );
490-
modelRoot.appendChildren( conversionResult );
491473

492474
expect( modelToString( conversionResult ) ).to.equal( '<div class="myClass"><abcd>foo</abcd></div>' );
493475
} );

tests/model/node.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ describe( 'Node', () => {
1616
one, two, three,
1717
textBA, textR, img;
1818

19-
before( () => {
19+
beforeEach( () => {
2020
node = new Node();
2121

2222
one = new Element( 'one' );

0 commit comments

Comments
 (0)