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

Commit 85e38e1

Browse files
author
Piotr Jasiun
authored
Merge pull request #1066 from ckeditor/t/1065
Fix: Prevented editor throwing during SplitDelta x RemoveDelta transformation when SplitDelta's first operation was neither InsertOperation nor ReinsertOperation. Closes #1065. Fix: Fixed remove model-to-view converter for some edge cases. Closes #1068.
2 parents b437856 + fb323da commit 85e38e1

File tree

5 files changed

+141
-8
lines changed

5 files changed

+141
-8
lines changed

src/conversion/model-to-view-converters.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import ViewElement from '../view/element';
77
import ViewText from '../view/text';
88
import ViewRange from '../view/range';
9+
import ViewPosition from '../view/position';
910
import ViewTreeWalker from '../view/treewalker';
1011
import viewWriter from '../view/writer';
1112

@@ -441,13 +442,22 @@ export function remove() {
441442
// end of that range is incorrect.
442443
// Instead we will use `data.sourcePosition` as this is the last correct model position and
443444
// it is a position before the removed item. Then, we will calculate view range to remove "manually".
444-
const viewPosition = conversionApi.mapper.toViewPosition( data.sourcePosition );
445+
let viewPosition = conversionApi.mapper.toViewPosition( data.sourcePosition );
445446
let viewRange;
446447

447448
if ( data.item.is( 'element' ) ) {
448449
// Note: in remove conversion we cannot use model-to-view element mapping because `data.item` may be
449450
// already mapped to another element (this happens when move change is converted).
450451
// In this case however, `viewPosition` is the position before view element that corresponds to removed model element.
452+
//
453+
// First, fix the position. Traverse the tree forward until the container element is found. The `viewPosition`
454+
// may be before a ui element, before attribute element or at the end of text element.
455+
viewPosition = viewPosition.getLastMatchingPosition( value => !value.item.is( 'containerElement' ) );
456+
457+
if ( viewPosition.parent.is( 'text' ) && viewPosition.isAtEnd ) {
458+
viewPosition = ViewPosition.createAfter( viewPosition.parent );
459+
}
460+
451461
viewRange = ViewRange.createOn( viewPosition.nodeAfter );
452462
} else {
453463
// If removed item is a text node, we need to traverse view tree to find the view range to remove.

src/model/delta/basic-transformations.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,13 @@ addTransformationCase( SplitDelta, RenameDelta, ( a, b, context ) => {
390390
// Add special case for RemoveDelta x SplitDelta transformation.
391391
addTransformationCase( RemoveDelta, SplitDelta, ( a, b, context ) => {
392392
const deltas = defaultTransform( a, b, context );
393-
const insertPosition = b._cloneOperation.position;
393+
// The "clone operation" may be InsertOperation, ReinsertOperation, MoveOperation or NoOperation.
394+
const insertPosition = b._cloneOperation.position || b._cloneOperation.targetPosition;
395+
396+
// NoOperation.
397+
if ( !insertPosition ) {
398+
return defaultTransform( a, b, context );
399+
}
394400

395401
// In case if `defaultTransform` returned more than one delta.
396402
for ( const delta of deltas ) {
@@ -413,9 +419,16 @@ addTransformationCase( SplitDelta, RemoveDelta, ( a, b, context ) => {
413419
// This case is very trickily solved.
414420
// Instead of fixing `a` delta, we change `b` delta for a while and fire default transformation with fixed `b` delta.
415421
// Thanks to that fixing `a` delta will be differently (correctly) transformed.
416-
b = b.clone();
422+
//
423+
// The "clone operation" may be InsertOperation, ReinsertOperation, MoveOperation or NoOperation.
424+
const insertPosition = a._cloneOperation.position || a._cloneOperation.targetPosition;
417425

418-
const insertPosition = a._cloneOperation.position;
426+
// NoOperation.
427+
if ( !insertPosition ) {
428+
return defaultTransform( a, b, context );
429+
}
430+
431+
b = b.clone();
419432
const operation = b._moveOperation;
420433
const rangeEnd = operation.sourcePosition.getShiftedBy( operation.howMany );
421434

tests/conversion/model-to-view-converters.js

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,8 +1028,8 @@ describe( 'model-to-view-converters', () => {
10281028
} );
10291029

10301030
it( 'should not unbind element that has not been moved to graveyard', () => {
1031-
const modelElement = new ModelElement( 'a' );
1032-
const viewElement = new ViewElement( 'a' );
1031+
const modelElement = new ModelElement( 'paragraph' );
1032+
const viewElement = new ViewContainerElement( 'p' );
10331033

10341034
modelRoot.appendChildren( [ modelElement, new ModelText( 'b' ) ] );
10351035
viewRoot.appendChildren( [ viewElement, new ViewText( 'b' ) ] );
@@ -1056,8 +1056,8 @@ describe( 'model-to-view-converters', () => {
10561056
} );
10571057

10581058
it( 'should unbind elements if model element was moved to graveyard', () => {
1059-
const modelElement = new ModelElement( 'a' );
1060-
const viewElement = new ViewElement( 'a' );
1059+
const modelElement = new ModelElement( 'paragraph' );
1060+
const viewElement = new ViewContainerElement( 'p' );
10611061

10621062
modelRoot.appendChildren( [ modelElement, new ModelText( 'b' ) ] );
10631063
viewRoot.appendChildren( [ viewElement, new ViewText( 'b' ) ] );
@@ -1125,5 +1125,62 @@ describe( 'model-to-view-converters', () => {
11251125
expect( mapper.toModelElement( viewWElement ) ).to.be.undefined;
11261126
expect( mapper.toViewElement( modelWElement ) ).to.be.undefined;
11271127
} );
1128+
1129+
it( 'should work correctly if container element after ui element is removed', () => {
1130+
const modelP1 = new ModelElement( 'paragraph' );
1131+
const modelP2 = new ModelElement( 'paragraph' );
1132+
1133+
const viewP1 = new ViewContainerElement( 'p' );
1134+
const viewUi1 = new ViewUIElement( 'span' );
1135+
const viewUi2 = new ViewUIElement( 'span' );
1136+
const viewP2 = new ViewContainerElement( 'p' );
1137+
1138+
modelRoot.appendChildren( [ modelP1, modelP2 ] );
1139+
viewRoot.appendChildren( [ viewP1, viewUi1, viewUi2, viewP2 ] );
1140+
1141+
mapper.bindElements( modelP1, viewP1 );
1142+
mapper.bindElements( modelP2, viewP2 );
1143+
1144+
dispatcher.on( 'remove', remove() );
1145+
1146+
modelWriter.move(
1147+
ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ),
1148+
ModelPosition.createAt( modelDoc.graveyard, 'end' )
1149+
);
1150+
1151+
dispatcher.convertRemove(
1152+
ModelPosition.createFromParentAndOffset( modelRoot, 1 ),
1153+
ModelRange.createFromParentsAndOffsets( modelDoc.graveyard, 0, modelDoc.graveyard, 1 )
1154+
);
1155+
1156+
expect( viewToString( viewRoot ) ).to.equal( '<div><p></p><span></span><span></span></div>' );
1157+
} );
1158+
1159+
it( 'should work correctly if container element after text node is removed', () => {
1160+
const modelText = new ModelText( 'foo' );
1161+
const modelP = new ModelElement( 'paragraph' );
1162+
1163+
const viewText = new ViewText( 'foo' );
1164+
const viewP = new ViewContainerElement( 'p' );
1165+
1166+
modelRoot.appendChildren( [ modelText, modelP ] );
1167+
viewRoot.appendChildren( [ viewText, viewP ] );
1168+
1169+
mapper.bindElements( modelP, viewP );
1170+
1171+
dispatcher.on( 'remove', remove() );
1172+
1173+
modelWriter.move(
1174+
ModelRange.createFromParentsAndOffsets( modelRoot, 3, modelRoot, 4 ),
1175+
ModelPosition.createAt( modelDoc.graveyard, 'end' )
1176+
);
1177+
1178+
dispatcher.convertRemove(
1179+
ModelPosition.createFromParentAndOffset( modelRoot, 3 ),
1180+
ModelRange.createFromParentsAndOffsets( modelDoc.graveyard, 0, modelDoc.graveyard, 1 )
1181+
);
1182+
1183+
expect( viewToString( viewRoot ) ).to.equal( '<div>foo</div>' );
1184+
} );
11281185
} );
11291186
} );

tests/model/delta/transform/removedelta.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,30 @@ describe( 'transform', () => {
184184
]
185185
} );
186186
} );
187+
188+
it( 'should not throw if clone operation is NoOperation and use default transformation in that case', () => {
189+
const noOpSplitDelta = new SplitDelta();
190+
noOpSplitDelta.addOperation( new NoOperation( 0 ) );
191+
noOpSplitDelta.addOperation( new MoveOperation( new Position( root, [ 1, 2 ] ), 3, new Position( root, [ 2, 0 ] ), 1 ) );
192+
193+
const removeDelta = getRemoveDelta( new Position( root, [ 3 ] ), 1, 0 );
194+
195+
const transformed = transform( removeDelta, noOpSplitDelta, context );
196+
197+
expect( transformed.length ).to.equal( 1 );
198+
199+
expectDelta( transformed[ 0 ], {
200+
type: RemoveDelta,
201+
operations: [
202+
{
203+
type: RemoveOperation,
204+
sourcePosition: new Position( root, [ 3 ] ),
205+
howMany: 1,
206+
baseVersion: 2
207+
}
208+
]
209+
} );
210+
} );
187211
} );
188212
} );
189213
} );

tests/model/delta/transform/splitdelta.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,35 @@ describe( 'transform', () => {
814814
]
815815
} );
816816
} );
817+
818+
it( 'should not throw if clone operation is NoOperation and use default transformation in that case', () => {
819+
const noOpSplitDelta = new SplitDelta();
820+
noOpSplitDelta.addOperation( new NoOperation( 0 ) );
821+
noOpSplitDelta.addOperation( new MoveOperation( new Position( root, [ 1, 2 ] ), 3, new Position( root, [ 2, 0 ] ), 1 ) );
822+
823+
const removeDelta = getRemoveDelta( new Position( root, [ 0 ] ), 1, 0 );
824+
825+
const transformed = transform( noOpSplitDelta, removeDelta, context );
826+
827+
expect( transformed.length ).to.equal( 1 );
828+
829+
expectDelta( transformed[ 0 ], {
830+
type: SplitDelta,
831+
operations: [
832+
{
833+
type: NoOperation,
834+
baseVersion: 1
835+
},
836+
{
837+
type: MoveOperation,
838+
sourcePosition: new Position( root, [ 0, 2 ] ),
839+
howMany: 3,
840+
targetPosition: new Position( root, [ 1, 0 ] ),
841+
baseVersion: 2
842+
}
843+
]
844+
} );
845+
} );
817846
} );
818847
} );
819848
} );

0 commit comments

Comments
 (0)