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

Fixed: Prevent throwing during SplitDelta x RemoveDelta transformation. #1066

Merged
merged 2 commits into from
Aug 14, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/conversion/model-to-view-converters.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import ViewElement from '../view/element';
import ViewText from '../view/text';
import ViewRange from '../view/range';
import ViewPosition from '../view/position';
import ViewTreeWalker from '../view/treewalker';
import viewWriter from '../view/writer';

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

if ( data.item.is( 'element' ) ) {
// Note: in remove conversion we cannot use model-to-view element mapping because `data.item` may be
// already mapped to another element (this happens when move change is converted).
// In this case however, `viewPosition` is the position before view element that corresponds to removed model element.
//
// First, fix the position. Traverse the tree forward until the container element is found. The `viewPosition`
// may be before a ui element, before attribute element or at the end of text element.
viewPosition = viewPosition.getLastMatchingPosition( value => !value.item.is( 'containerElement' ) );

if ( viewPosition.parent.is( 'text' ) && viewPosition.isAtEnd ) {
viewPosition = ViewPosition.createAfter( viewPosition.parent );
}

viewRange = ViewRange.createOn( viewPosition.nodeAfter );
} else {
// If removed item is a text node, we need to traverse view tree to find the view range to remove.
Expand Down
19 changes: 16 additions & 3 deletions src/model/delta/basic-transformations.js
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,13 @@ addTransformationCase( SplitDelta, RenameDelta, ( a, b, context ) => {
// Add special case for RemoveDelta x SplitDelta transformation.
addTransformationCase( RemoveDelta, SplitDelta, ( a, b, context ) => {
const deltas = defaultTransform( a, b, context );
const insertPosition = b._cloneOperation.position;
// The "clone operation" may be InsertOperation, ReinsertOperation, MoveOperation or NoOperation.
const insertPosition = b._cloneOperation.position || b._cloneOperation.targetPosition;

// NoOperation.
if ( !insertPosition ) {
return defaultTransform( a, b, context );
}

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

const insertPosition = a._cloneOperation.position;
// NoOperation.
if ( !insertPosition ) {
return defaultTransform( a, b, context );
}

b = b.clone();
const operation = b._moveOperation;
const rangeEnd = operation.sourcePosition.getShiftedBy( operation.howMany );

Expand Down
65 changes: 61 additions & 4 deletions tests/conversion/model-to-view-converters.js
Original file line number Diff line number Diff line change
Expand Up @@ -1028,8 +1028,8 @@ describe( 'model-to-view-converters', () => {
} );

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

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

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

modelRoot.appendChildren( [ modelElement, new ModelText( 'b' ) ] );
viewRoot.appendChildren( [ viewElement, new ViewText( 'b' ) ] );
Expand Down Expand Up @@ -1125,5 +1125,62 @@ describe( 'model-to-view-converters', () => {
expect( mapper.toModelElement( viewWElement ) ).to.be.undefined;
expect( mapper.toViewElement( modelWElement ) ).to.be.undefined;
} );

it( 'should work correctly if container element after ui element is removed', () => {
const modelP1 = new ModelElement( 'paragraph' );
const modelP2 = new ModelElement( 'paragraph' );

const viewP1 = new ViewContainerElement( 'p' );
const viewUi1 = new ViewUIElement( 'span' );
const viewUi2 = new ViewUIElement( 'span' );
const viewP2 = new ViewContainerElement( 'p' );

modelRoot.appendChildren( [ modelP1, modelP2 ] );
viewRoot.appendChildren( [ viewP1, viewUi1, viewUi2, viewP2 ] );

mapper.bindElements( modelP1, viewP1 );
mapper.bindElements( modelP2, viewP2 );

dispatcher.on( 'remove', remove() );

modelWriter.move(
ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ),
ModelPosition.createAt( modelDoc.graveyard, 'end' )
);

dispatcher.convertRemove(
ModelPosition.createFromParentAndOffset( modelRoot, 1 ),
ModelRange.createFromParentsAndOffsets( modelDoc.graveyard, 0, modelDoc.graveyard, 1 )
);

expect( viewToString( viewRoot ) ).to.equal( '<div><p></p><span></span><span></span></div>' );
} );

it( 'should work correctly if container element after text node is removed', () => {
const modelText = new ModelText( 'foo' );
const modelP = new ModelElement( 'paragraph' );

const viewText = new ViewText( 'foo' );
const viewP = new ViewContainerElement( 'p' );

modelRoot.appendChildren( [ modelText, modelP ] );
viewRoot.appendChildren( [ viewText, viewP ] );

mapper.bindElements( modelP, viewP );

dispatcher.on( 'remove', remove() );

modelWriter.move(
ModelRange.createFromParentsAndOffsets( modelRoot, 3, modelRoot, 4 ),
ModelPosition.createAt( modelDoc.graveyard, 'end' )
);

dispatcher.convertRemove(
ModelPosition.createFromParentAndOffset( modelRoot, 3 ),
ModelRange.createFromParentsAndOffsets( modelDoc.graveyard, 0, modelDoc.graveyard, 1 )
);

expect( viewToString( viewRoot ) ).to.equal( '<div>foo</div>' );
} );
} );
} );
24 changes: 24 additions & 0 deletions tests/model/delta/transform/removedelta.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,30 @@ describe( 'transform', () => {
]
} );
} );

it( 'should not throw if clone operation is NoOperation and use default transformation in that case', () => {
const noOpSplitDelta = new SplitDelta();
noOpSplitDelta.addOperation( new NoOperation( 0 ) );
noOpSplitDelta.addOperation( new MoveOperation( new Position( root, [ 1, 2 ] ), 3, new Position( root, [ 2, 0 ] ), 1 ) );

const removeDelta = getRemoveDelta( new Position( root, [ 3 ] ), 1, 0 );

const transformed = transform( removeDelta, noOpSplitDelta, context );

expect( transformed.length ).to.equal( 1 );

expectDelta( transformed[ 0 ], {
type: RemoveDelta,
operations: [
{
type: RemoveOperation,
sourcePosition: new Position( root, [ 3 ] ),
howMany: 1,
baseVersion: 2
}
]
} );
} );
} );
} );
} );
29 changes: 29 additions & 0 deletions tests/model/delta/transform/splitdelta.js
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,35 @@ describe( 'transform', () => {
]
} );
} );

it( 'should not throw if clone operation is NoOperation and use default transformation in that case', () => {
const noOpSplitDelta = new SplitDelta();
noOpSplitDelta.addOperation( new NoOperation( 0 ) );
noOpSplitDelta.addOperation( new MoveOperation( new Position( root, [ 1, 2 ] ), 3, new Position( root, [ 2, 0 ] ), 1 ) );

const removeDelta = getRemoveDelta( new Position( root, [ 0 ] ), 1, 0 );

const transformed = transform( noOpSplitDelta, removeDelta, context );

expect( transformed.length ).to.equal( 1 );

expectDelta( transformed[ 0 ], {
type: SplitDelta,
operations: [
{
type: NoOperation,
baseVersion: 1
},
{
type: MoveOperation,
sourcePosition: new Position( root, [ 0, 2 ] ),
howMany: 3,
targetPosition: new Position( root, [ 1, 0 ] ),
baseVersion: 2
}
]
} );
} );
} );
} );
} );